Upgrade python/apitools to v0.5.31

Exempt-From-Owner-Approval:
Change-Id: I87e804ff19a93d51d6244f834bceebc32536801e
diff --git a/.travis.yml b/.travis.yml
index 627bf3d..453e56d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,4 @@
 language: python
-sudo: false
 matrix:
   include:
   - python: "2.7"
@@ -12,8 +11,6 @@
     env: TOX_ENV=py27-oauth2client3
   - python: "2.7"
     env: TOX_ENV=py27-oauth2client4
-  - python: "3.4"
-    env: TOX_ENV=py34-oauth2client4
   - python: "3.5"
     env: TOX_ENV=py35-oauth2client1
   - python: "3.5"
diff --git a/METADATA b/METADATA
index 536369d..e5ecb93 100644
--- a/METADATA
+++ b/METADATA
@@ -9,11 +9,11 @@
     type: GIT
     value: "https://github.com/google/apitools"
   }
-  version: "v0.5.30"
+  version: "v0.5.31"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2019
-    month: 6
-    day: 26
+    year: 2020
+    month: 5
+    day: 14
   }
 }
diff --git a/apitools/base/protorpclite/descriptor.py b/apitools/base/protorpclite/descriptor.py
index add0e4c..70b7ed1 100644
--- a/apitools/base/protorpclite/descriptor.py
+++ b/apitools/base/protorpclite/descriptor.py
@@ -292,7 +292,7 @@
     enum_descriptor.name = enum_definition.definition_name().split('.')[-1]
 
     values = []
-    for number in enum_definition.numbers():
+    for number in sorted(enum_definition.numbers()):
         value = enum_definition.lookup_by_number(number)
         values.append(describe_enum_value(value))
 
diff --git a/apitools/base/protorpclite/descriptor_test.py b/apitools/base/protorpclite/descriptor_test.py
index fc27ec4..5fbed35 100644
--- a/apitools/base/protorpclite/descriptor_test.py
+++ b/apitools/base/protorpclite/descriptor_test.py
@@ -18,9 +18,9 @@
 """Tests for apitools.base.protorpclite.descriptor."""
 import platform
 import types
+import unittest
 
 import six
-import unittest2
 
 from apitools.base.protorpclite import descriptor
 from apitools.base.protorpclite import message_types
@@ -78,8 +78,8 @@
         described.check_initialized()
         self.assertEquals(expected, described)
 
-    @unittest2.skipIf('PyPy' in platform.python_implementation(),
-                      'todo: reenable this')
+    @unittest.skipIf('PyPy' in platform.python_implementation(),
+                     'todo: reenable this')
     def testEnumWithItems(self):
         class EnumWithItems(messages.Enum):
             A = 3
@@ -512,4 +512,4 @@
 
 
 if __name__ == '__main__':
-    unittest2.main()
+    unittest.main()
diff --git a/apitools/base/protorpclite/test_util.py b/apitools/base/protorpclite/test_util.py
index 43345fc..89e3a68 100644
--- a/apitools/base/protorpclite/test_util.py
+++ b/apitools/base/protorpclite/test_util.py
@@ -33,10 +33,10 @@
 import re
 import socket
 import types
+import unittest
 
 import six
 from six.moves import range  # pylint: disable=redefined-builtin
-import unittest2 as unittest
 
 from apitools.base.protorpclite import message_types
 from apitools.base.protorpclite import messages
diff --git a/apitools/base/py/base_api_test.py b/apitools/base/py/base_api_test.py
index b00085c..27b1727 100644
--- a/apitools/base/py/base_api_test.py
+++ b/apitools/base/py/base_api_test.py
@@ -17,11 +17,11 @@
 import datetime
 import sys
 import contextlib
+import unittest
 
 import six
 from six.moves import http_client
 from six.moves import urllib_parse
-import unittest2
 
 from apitools.base.protorpclite import message_types
 from apitools.base.protorpclite import messages
@@ -96,7 +96,7 @@
         super(FakeService, self).__init__(client)
 
 
-class BaseApiTest(unittest2.TestCase):
+class BaseApiTest(unittest.TestCase):
 
     def __GetFakeClient(self):
         return FakeClient('', credentials=FakeCredentials())
@@ -331,4 +331,4 @@
 
 
 if __name__ == '__main__':
-    unittest2.main()
+    unittest.main()
diff --git a/apitools/base/py/batch_test.py b/apitools/base/py/batch_test.py
index 0574dc6..90cf4fb 100644
--- a/apitools/base/py/batch_test.py
+++ b/apitools/base/py/batch_test.py
@@ -16,12 +16,12 @@
 """Tests for apitools.base.py.batch."""
 
 import textwrap
+import unittest
 
 import mock
 from six.moves import http_client
 from six.moves import range  # pylint:disable=redefined-builtin
 from six.moves.urllib import parse
-import unittest2
 
 from apitools.base.py import batch
 from apitools.base.py import exceptions
@@ -69,7 +69,7 @@
         return http_response
 
 
-class BatchTest(unittest2.TestCase):
+class BatchTest(unittest.TestCase):
 
     def assertUrlEqual(self, expected_url, provided_url):
 
diff --git a/apitools/base/py/buffered_stream_test.py b/apitools/base/py/buffered_stream_test.py
index 2098fb1..4de231d 100644
--- a/apitools/base/py/buffered_stream_test.py
+++ b/apitools/base/py/buffered_stream_test.py
@@ -16,15 +16,15 @@
 """Tests for buffered_stream."""
 
 import string
+import unittest
 
 import six
-import unittest2
 
 from apitools.base.py import buffered_stream
 from apitools.base.py import exceptions
 
 
-class BufferedStreamTest(unittest2.TestCase):
+class BufferedStreamTest(unittest.TestCase):
 
     def setUp(self):
         self.stream = six.StringIO(string.ascii_letters)
diff --git a/apitools/base/py/compression_test.py b/apitools/base/py/compression_test.py
index c8ecdac..9832b31 100644
--- a/apitools/base/py/compression_test.py
+++ b/apitools/base/py/compression_test.py
@@ -16,14 +16,15 @@
 
 """Tests for compression."""
 
+import unittest
+
 from apitools.base.py import compression
 from apitools.base.py import gzip
 
 import six
-import unittest2
 
 
-class CompressionTest(unittest2.TestCase):
+class CompressionTest(unittest.TestCase):
 
     def setUp(self):
         # Sample highly compressible data (~50MB).
@@ -98,7 +99,7 @@
         self.assertTrue(exhausted)
 
 
-class StreamingBufferTest(unittest2.TestCase):
+class StreamingBufferTest(unittest.TestCase):
 
     def setUp(self):
         self.stream = compression.StreamingBuffer()
diff --git a/apitools/base/py/credentials_lib.py b/apitools/base/py/credentials_lib.py
index bf39285..0823f93 100644
--- a/apitools/base/py/credentials_lib.py
+++ b/apitools/base/py/credentials_lib.py
@@ -17,6 +17,7 @@
 """Common credentials classes and constructors."""
 from __future__ import print_function
 
+import argparse
 import contextlib
 import datetime
 import json
@@ -515,10 +516,6 @@
     # since they're bringing their own credentials. So we just allow this
     # to fail with an ImportError in those cases.
     #
-    # TODO(craigcitro): Move this import back to the top when we drop
-    # python 2.6 support (eg when gsutil does).
-    import argparse
-
     parser = argparse.ArgumentParser(parents=[tools.argparser])
     # Get command line argparse flags.
     flags, _ = parser.parse_known_args(args=args)
@@ -721,10 +718,6 @@
         client_info, service_account_name=None, service_account_keyfile=None,
         service_account_json_keyfile=None, **unused_kwds):
     """Returns ServiceAccountCredentials from give file."""
-    if ((service_account_name and not service_account_keyfile) or
-            (service_account_keyfile and not service_account_name)):
-        raise exceptions.CredentialsError(
-            'Service account name or keyfile provided without the other')
     scopes = client_info['scope'].split()
     user_agent = client_info['user_agent']
     # Use the .json credentials, if provided.
@@ -732,6 +725,10 @@
         return ServiceAccountCredentialsFromFile(
             service_account_json_keyfile, scopes, user_agent=user_agent)
     # Fall back to .p12 if there's no .json credentials.
+    if ((service_account_name and not service_account_keyfile) or
+            (service_account_keyfile and not service_account_name)):
+        raise exceptions.CredentialsError(
+            'Service account name or keyfile provided without the other')
     if service_account_name is not None:
         return ServiceAccountCredentialsFromP12File(
             service_account_name, service_account_keyfile, scopes, user_agent)
diff --git a/apitools/base/py/credentials_lib_test.py b/apitools/base/py/credentials_lib_test.py
index 80b970c..64f056d 100644
--- a/apitools/base/py/credentials_lib_test.py
+++ b/apitools/base/py/credentials_lib_test.py
@@ -17,10 +17,10 @@
 import os.path
 import shutil
 import tempfile
+import unittest
 
 import mock
 import six
-import unittest2
 
 from apitools.base.py import credentials_lib
 from apitools.base.py import util
@@ -43,7 +43,7 @@
         self.fail('Unexpected HTTP request to %s' % request_url)
 
 
-class CredentialsLibTest(unittest2.TestCase):
+class CredentialsLibTest(unittest.TestCase):
 
     def _RunGceAssertionCredentials(
             self, service_account_name=None, scopes=None, cache_filename=None):
@@ -153,7 +153,7 @@
         self.assertIsNone(creds)
 
 
-class TestGetRunFlowFlags(unittest2.TestCase):
+class TestGetRunFlowFlags(unittest.TestCase):
 
     def setUp(self):
         self._flags_actual = credentials_lib.FLAGS
diff --git a/apitools/base/py/encoding_test.py b/apitools/base/py/encoding_test.py
index d130cc5..54058a2 100644
--- a/apitools/base/py/encoding_test.py
+++ b/apitools/base/py/encoding_test.py
@@ -17,8 +17,7 @@
 import datetime
 import json
 import sys
-
-import unittest2
+import unittest
 
 from apitools.base.protorpclite import message_types
 from apitools.base.protorpclite import messages
@@ -238,7 +237,7 @@
                                    'repeated_field', 'repeatedField')
 
 
-class EncodingTest(unittest2.TestCase):
+class EncodingTest(unittest.TestCase):
 
     def testCopyProtoMessage(self):
         msg = SimpleMessage(field='abc')
diff --git a/apitools/base/py/exceptions_test.py b/apitools/base/py/exceptions_test.py
index 4937f73..6e3a182 100644
--- a/apitools/base/py/exceptions_test.py
+++ b/apitools/base/py/exceptions_test.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest2
+import unittest
 
 from apitools.base.py import exceptions
 from apitools.base.py import http_wrapper
@@ -24,7 +24,7 @@
         request_url='http://www.google.com')
 
 
-class HttpErrorFromResponseTest(unittest2.TestCase):
+class HttpErrorFromResponseTest(unittest.TestCase):
 
     """Tests for exceptions.HttpError.FromResponse."""
 
diff --git a/apitools/base/py/extra_types.py b/apitools/base/py/extra_types.py
index 847dc91..e40a785 100644
--- a/apitools/base/py/extra_types.py
+++ b/apitools/base/py/extra_types.py
@@ -16,7 +16,6 @@
 
 """Extra types understood by apitools."""
 
-import collections
 import datetime
 import json
 import numbers
@@ -30,6 +29,11 @@
 from apitools.base.py import exceptions
 from apitools.base.py import util
 
+if six.PY3:
+    from collections.abc import Iterable
+else:
+    from collections import Iterable
+
 __all__ = [
     'DateField',
     'DateTimeMessage',
@@ -129,7 +133,7 @@
         return JsonValue(double_value=float(py_value))
     if isinstance(py_value, dict):
         return JsonValue(object_value=_PythonValueToJsonObject(py_value))
-    if isinstance(py_value, collections.Iterable):
+    if isinstance(py_value, Iterable):
         return JsonValue(array_value=_PythonValueToJsonArray(py_value))
     raise exceptions.InvalidDataError(
         'Cannot convert "%s" to JsonValue' % py_value)
@@ -212,7 +216,7 @@
 def _PythonValueToJsonProto(py_value):
     if isinstance(py_value, dict):
         return _PythonValueToJsonObject(py_value)
-    if (isinstance(py_value, collections.Iterable) and
+    if (isinstance(py_value, Iterable) and
             not isinstance(py_value, six.string_types)):
         return _PythonValueToJsonArray(py_value)
     return _PythonValueToJsonValue(py_value)
diff --git a/apitools/base/py/extra_types_test.py b/apitools/base/py/extra_types_test.py
index 7e37f7c..6a4092b 100644
--- a/apitools/base/py/extra_types_test.py
+++ b/apitools/base/py/extra_types_test.py
@@ -16,8 +16,7 @@
 import datetime
 import json
 import math
-
-import unittest2
+import unittest
 
 from apitools.base.protorpclite import messages
 from apitools.base.py import encoding
@@ -25,7 +24,7 @@
 from apitools.base.py import extra_types
 
 
-class ExtraTypesTest(unittest2.TestCase):
+class ExtraTypesTest(unittest.TestCase):
 
     def assertRoundTrip(self, value):
         if isinstance(value, extra_types._JSON_PROTO_TYPES):
diff --git a/apitools/base/py/http_wrapper.py b/apitools/base/py/http_wrapper.py
index a3fe65c..c31bea0 100644
--- a/apitools/base/py/http_wrapper.py
+++ b/apitools/base/py/http_wrapper.py
@@ -339,6 +339,10 @@
     """
     retry = 0
     first_req_time = time.time()
+    # Provide compatibility for breaking change in httplib2 0.16.0+:
+    # https://github.com/googleapis/google-api-python-client/issues/803
+    if hasattr(http, 'redirect_codes'):
+        http.redirect_codes = set(http.redirect_codes) - {308}
     while True:
         try:
             return _MakeRequestNoRetry(
diff --git a/apitools/base/py/http_wrapper_test.py b/apitools/base/py/http_wrapper_test.py
index ce4c03e..fdf56f5 100644
--- a/apitools/base/py/http_wrapper_test.py
+++ b/apitools/base/py/http_wrapper_test.py
@@ -15,10 +15,10 @@
 
 """Tests for http_wrapper."""
 import socket
+import unittest
 
 import httplib2
 from six.moves import http_client
-import unittest2
 
 from mock import patch
 
@@ -57,7 +57,7 @@
         return 1
 
 
-class HttpWrapperTest(unittest2.TestCase):
+class HttpWrapperTest(unittest.TestCase):
 
     def testRequestBodyUsesLengthProperty(self):
         http_wrapper.Request(body=RaisesExceptionOnLen())
@@ -65,8 +65,8 @@
     def testRequestBodyWithLen(self):
         http_wrapper.Request(body='burrito')
 
-    @unittest2.skipIf(not _TOKEN_REFRESH_STATUS_AVAILABLE,
-                      'oauth2client<1.5 lacks HttpAccessTokenRefreshError.')
+    @unittest.skipIf(not _TOKEN_REFRESH_STATUS_AVAILABLE,
+                     'oauth2client<1.5 lacks HttpAccessTokenRefreshError.')
     def testExceptionHandlerHttpAccessTokenError(self):
         exception_arg = HttpAccessTokenRefreshError(status=503)
         retry_args = http_wrapper.ExceptionRetryArgs(
@@ -80,8 +80,8 @@
             http_wrapper.HandleExceptionsAndRebuildHttpConnections(
                 retry_args)
 
-    @unittest2.skipIf(not _TOKEN_REFRESH_STATUS_AVAILABLE,
-                      'oauth2client<1.5 lacks HttpAccessTokenRefreshError.')
+    @unittest.skipIf(not _TOKEN_REFRESH_STATUS_AVAILABLE,
+                     'oauth2client<1.5 lacks HttpAccessTokenRefreshError.')
     def testExceptionHandlerHttpAccessTokenErrorRaises(self):
         exception_arg = HttpAccessTokenRefreshError(status=200)
         retry_args = http_wrapper.ExceptionRetryArgs(
diff --git a/apitools/base/py/list_pager.py b/apitools/base/py/list_pager.py
index fb14c14..a2c1080 100644
--- a/apitools/base/py/list_pager.py
+++ b/apitools/base/py/list_pager.py
@@ -17,18 +17,58 @@
 """A helper function that executes a series of List queries for many APIs."""
 
 from apitools.base.py import encoding
+import six
 
 __all__ = [
     'YieldFromList',
 ]
 
 
+def _GetattrNested(message, attribute):
+    """Gets a possibly nested attribute.
+
+    Same as getattr() if attribute is a string;
+    if attribute is a tuple, returns the nested attribute referred to by
+    the fields in the tuple as if they were a dotted accessor path.
+
+    (ex _GetattrNested(msg, ('foo', 'bar', 'baz')) gets msg.foo.bar.baz
+    """
+    if isinstance(attribute, six.string_types):
+        return getattr(message, attribute)
+    elif len(attribute) == 0:
+        return message
+    else:
+        return _GetattrNested(getattr(message, attribute[0]), attribute[1:])
+
+
+def _SetattrNested(message, attribute, value):
+    """Sets a possibly nested attribute.
+
+    Same as setattr() if attribute is a string;
+    if attribute is a tuple, sets the nested attribute referred to by
+    the fields in the tuple as if they were a dotted accessor path.
+
+    (ex _SetattrNested(msg, ('foo', 'bar', 'baz'), 'v') sets msg.foo.bar.baz
+    to 'v'
+    """
+    if isinstance(attribute, six.string_types):
+        return setattr(message, attribute, value)
+    elif len(attribute) < 1:
+        raise ValueError("Need an attribute to set")
+    elif len(attribute) == 1:
+        return setattr(message, attribute[0], value)
+    else:
+        return setattr(_GetattrNested(message, attribute[:-1]),
+                       attribute[-1], value)
+
+
 def YieldFromList(
         service, request, global_params=None, limit=None, batch_size=100,
         method='List', field='items', predicate=None,
         current_token_attribute='pageToken',
         next_token_attribute='nextPageToken',
-        batch_size_attribute='maxResults'):
+        batch_size_attribute='maxResults',
+        get_field_func=_GetattrNested):
     """Make a series of List requests, keeping track of page tokens.
 
     Args:
@@ -45,21 +85,25 @@
       method: str, The name of the method used to fetch resources.
       field: str, The field in the response that will be a list of items.
       predicate: lambda, A function that returns true for items to be yielded.
-      current_token_attribute: str, The name of the attribute in a
+      current_token_attribute: str or tuple, The name of the attribute in a
           request message holding the page token for the page being
-          requested.
-      next_token_attribute: str, The name of the attribute in a
-          response message holding the page token for the next page.
-      batch_size_attribute: str, The name of the attribute in a
+          requested. If a tuple, path to attribute.
+      next_token_attribute: str or tuple, The name of the attribute in a
+          response message holding the page token for the next page. If a
+          tuple, path to the attribute.
+      batch_size_attribute: str or tuple, The name of the attribute in a
           response message holding the maximum number of results to be
           returned. None if caller-specified batch size is unsupported.
+          If a tuple, path to the attribute.
+      get_field_func: Function that returns the items to be yielded. Argument
+          is response message, and field.
 
     Yields:
       protorpc.message.Message, The resources listed by the service.
 
     """
     request = encoding.CopyProtoMessage(request)
-    setattr(request, current_token_attribute, None)
+    _SetattrNested(request, current_token_attribute, None)
     while limit is None or limit:
         if batch_size_attribute:
             # On Py3, None is not comparable so min() below will fail.
@@ -72,10 +116,10 @@
                 request_batch_size = None
             else:
                 request_batch_size = min(batch_size, limit or batch_size)
-            setattr(request, batch_size_attribute, request_batch_size)
+            _SetattrNested(request, batch_size_attribute, request_batch_size)
         response = getattr(service, method)(request,
                                             global_params=global_params)
-        items = getattr(response, field)
+        items = get_field_func(response, field)
         if predicate:
             items = list(filter(predicate, items))
         for item in items:
@@ -85,7 +129,7 @@
             limit -= 1
             if not limit:
                 return
-        token = getattr(response, next_token_attribute)
+        token = _GetattrNested(response, next_token_attribute)
         if not token:
             return
-        setattr(request, current_token_attribute, token)
+        _SetattrNested(request, current_token_attribute, token)
diff --git a/apitools/base/py/list_pager_test.py b/apitools/base/py/list_pager_test.py
index 32dfea6..1ea6368 100644
--- a/apitools/base/py/list_pager_test.py
+++ b/apitools/base/py/list_pager_test.py
@@ -15,7 +15,7 @@
 
 """Tests for list_pager."""
 
-import unittest2
+import unittest
 
 from apitools.base.py import list_pager
 from apitools.base.py.testing import mock
@@ -27,7 +27,33 @@
 from samples.iam_sample.iam_v1 import iam_v1_messages as iam_messages
 
 
-class ListPagerTest(unittest2.TestCase):
+class Example(object):
+    def __init__(self):
+        self.a = 'aaa'
+        self.b = 'bbb'
+        self.c = 'ccc'
+
+
+class GetterSetterTest(unittest.TestCase):
+
+    def testGetattrNested(self):
+        o = Example()
+        self.assertEqual(list_pager._GetattrNested(o, 'a'), 'aaa')
+        self.assertEqual(list_pager._GetattrNested(o, ('a',)), 'aaa')
+        o.b = Example()
+        self.assertEqual(list_pager._GetattrNested(o, ('b', 'c')), 'ccc')
+
+    def testSetattrNested(self):
+        o = Example()
+        list_pager._SetattrNested(o, 'b', Example())
+        self.assertEqual(o.b.a, 'aaa')
+        list_pager._SetattrNested(o, ('b', 'a'), 'AAA')
+        self.assertEqual(o.b.a, 'AAA')
+        list_pager._SetattrNested(o, ('c',), 'CCC')
+        self.assertEqual(o.c, 'CCC')
+
+
+class ListPagerTest(unittest.TestCase):
 
     def _AssertInstanceSequence(self, results, n):
         counter = 0
@@ -242,8 +268,34 @@
 
         self._AssertInstanceSequence(results, 3)
 
+    def testYieldFromListWithCustomGetFieldFunction(self):
+        self.mocked_client.column.List.Expect(
+            messages.FusiontablesColumnListRequest(
+                maxResults=100,
+                pageToken=None,
+                tableId='mytable',
+            ),
+            messages.ColumnList(
+                items=[
+                    messages.Column(name='c0')
+                ]
+            ))
+        custom_getter_called = []
 
-class ListPagerAttributeTest(unittest2.TestCase):
+        def Custom_Getter(message, attribute):
+            custom_getter_called.append(True)
+            return getattr(message, attribute)
+
+        client = fusiontables.FusiontablesV1(get_credentials=False)
+        request = messages.FusiontablesColumnListRequest(tableId='mytable')
+        results = list_pager.YieldFromList(
+            client.column, request, get_field_func=Custom_Getter)
+
+        self._AssertInstanceSequence(results, 1)
+        self.assertEquals(1, len(custom_getter_called))
+
+
+class ListPagerAttributeTest(unittest.TestCase):
 
     def setUp(self):
         self.mocked_client = mock.Client(iam_client.IamV1)
diff --git a/apitools/base/py/stream_slice_test.py b/apitools/base/py/stream_slice_test.py
index 4d5cdfb..f29e112 100644
--- a/apitools/base/py/stream_slice_test.py
+++ b/apitools/base/py/stream_slice_test.py
@@ -16,15 +16,15 @@
 """Tests for stream_slice."""
 
 import string
+import unittest
 
 import six
-import unittest2
 
 from apitools.base.py import exceptions
 from apitools.base.py import stream_slice
 
 
-class StreamSliceTest(unittest2.TestCase):
+class StreamSliceTest(unittest.TestCase):
 
     def setUp(self):
         self.stream = six.StringIO(string.ascii_letters)
diff --git a/apitools/base/py/testing/mock.py b/apitools/base/py/testing/mock.py
index 3bd38ba..ae6ad89 100644
--- a/apitools/base/py/testing/mock.py
+++ b/apitools/base/py/testing/mock.py
@@ -170,7 +170,8 @@
           The response that was specified to be returned.
 
         """
-        if key != self.__key or not _MessagesEqual(request, self.__request):
+        if key != self.__key or not (self.__request == request or
+                                     _MessagesEqual(request, self.__request)):
             raise UnexpectedRequestException((key, request),
                                              (self.__key, self.__request))
 
diff --git a/apitools/base/py/testing/mock_test.py b/apitools/base/py/testing/mock_test.py
index 4afdf7b..9bd8f05 100644
--- a/apitools/base/py/testing/mock_test.py
+++ b/apitools/base/py/testing/mock_test.py
@@ -15,8 +15,9 @@
 
 """Tests for apitools.base.py.testing.mock."""
 
+import unittest
+
 import httplib2
-import unittest2
 import six
 
 from apitools.base.protorpclite import messages
@@ -42,7 +43,7 @@
     pass
 
 
-class MockTest(unittest2.TestCase):
+class MockTest(unittest.TestCase):
 
     def testMockFusionBasic(self):
         with mock.Client(fusiontables.FusiontablesV1) as client_class:
@@ -151,6 +152,38 @@
         client = fusiontables.FusiontablesV1(get_credentials=False)
         self.assertNotEqual(type(client.column), mocked_service_type)
 
+    def testRequestMacher(self):
+        class Matcher(object):
+            def __init__(self, eq):
+                self._eq = eq
+
+            def __eq__(self, other):
+                return self._eq(other)
+
+        with mock.Client(fusiontables.FusiontablesV1) as client_class:
+            def IsEven(x):
+                return x % 2 == 0
+
+            def IsOdd(x):
+                return not IsEven(x)
+
+            client_class.column.List.Expect(
+                request=Matcher(IsEven), response=1,
+                enable_type_checking=False)
+            client_class.column.List.Expect(
+                request=Matcher(IsOdd), response=2, enable_type_checking=False)
+            client_class.column.List.Expect(
+                request=Matcher(IsEven), response=3,
+                enable_type_checking=False)
+            client_class.column.List.Expect(
+                request=Matcher(IsOdd), response=4, enable_type_checking=False)
+
+            client = fusiontables.FusiontablesV1(get_credentials=False)
+            self.assertEqual(client.column.List(2), 1)
+            self.assertEqual(client.column.List(1), 2)
+            self.assertEqual(client.column.List(20), 3)
+            self.assertEqual(client.column.List(23), 4)
+
     def testClientUnmock(self):
         mock_client = mock.Client(fusiontables.FusiontablesV1)
         self.assertFalse(isinstance(mock_client, fusiontables.FusiontablesV1))
@@ -220,7 +253,7 @@
     nested = messages.MessageField(_NestedMessage, 1)
 
 
-class UtilTest(unittest2.TestCase):
+class UtilTest(unittest.TestCase):
 
     def testMessagesEqual(self):
         self.assertFalse(mock._MessagesEqual(
diff --git a/apitools/base/py/transfer_test.py b/apitools/base/py/transfer_test.py
index c68e77e..4a9e79c 100644
--- a/apitools/base/py/transfer_test.py
+++ b/apitools/base/py/transfer_test.py
@@ -16,12 +16,12 @@
 
 """Tests for transfer.py."""
 import string
+import unittest
 
 import httplib2
 import mock
 import six
 from six.moves import http_client
-import unittest2
 
 from apitools.base.py import base_api
 from apitools.base.py import exceptions
@@ -30,7 +30,7 @@
 from apitools.base.py import transfer
 
 
-class TransferTest(unittest2.TestCase):
+class TransferTest(unittest.TestCase):
 
     def assertRangeAndContentRangeCompatible(self, request, response):
         request_prefix = 'bytes='
@@ -311,7 +311,7 @@
             self.assertTrue(rewritten_upload_contents.endswith(upload_bytes))
 
 
-class UploadTest(unittest2.TestCase):
+class UploadTest(unittest.TestCase):
 
     def setUp(self):
         # Sample highly compressible data.
diff --git a/apitools/base/py/util.py b/apitools/base/py/util.py
index ac1a44c..ad086e4 100644
--- a/apitools/base/py/util.py
+++ b/apitools/base/py/util.py
@@ -16,7 +16,6 @@
 
 """Assorted utilities shared between parts of apitools."""
 
-import collections
 import os
 import random
 
@@ -30,6 +29,11 @@
 from apitools.base.py import encoding_helper as encoding
 from apitools.base.py import exceptions
 
+if six.PY3:
+    from collections.abc import Iterable
+else:
+    from collections import Iterable
+
 __all__ = [
     'DetectGae',
     'DetectGce',
@@ -78,7 +82,7 @@
     if isinstance(scope_spec, six.string_types):
         scope_spec = six.ensure_str(scope_spec)
         return set(scope_spec.split(' '))
-    elif isinstance(scope_spec, collections.Iterable):
+    elif isinstance(scope_spec, Iterable):
         scope_spec = [six.ensure_str(x) for x in scope_spec]
         return set(scope_spec)
     raise exceptions.TypecheckError(
diff --git a/apitools/base/py/util_test.py b/apitools/base/py/util_test.py
index b2ece27..c3a4732 100644
--- a/apitools/base/py/util_test.py
+++ b/apitools/base/py/util_test.py
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 """Tests for util.py."""
-import unittest2
+import unittest
 
 from apitools.base.protorpclite import messages
 from apitools.base.py import encoding
@@ -48,7 +48,7 @@
     MessageWithRemappings.AnEnum, 'value_one', 'ONE')
 
 
-class UtilTest(unittest2.TestCase):
+class UtilTest(unittest.TestCase):
 
     def testExpand(self):
         method_config_xy = MockedMethodConfig(relative_path='{x}/y/{z}',
diff --git a/apitools/gen/client_generation_test.py b/apitools/gen/client_generation_test.py
index 9146501..4e382dd 100644
--- a/apitools/gen/client_generation_test.py
+++ b/apitools/gen/client_generation_test.py
@@ -22,15 +22,11 @@
 import subprocess
 import sys
 import tempfile
+import unittest
 
 from apitools.gen import gen_client
 from apitools.gen import test_utils
 
-if six.PY2:
-    import unittest2 as unittest
-else:
-    import unittest
-
 _API_LIST = [
     'bigquery.v2',
     'compute.v1',
diff --git a/apitools/gen/gen_client_test.py b/apitools/gen/gen_client_test.py
index 6c4e9b1..a0f30d5 100644
--- a/apitools/gen/gen_client_test.py
+++ b/apitools/gen/gen_client_test.py
@@ -16,8 +16,7 @@
 """Test for gen_client module."""
 
 import os
-
-import unittest2
+import unittest
 
 from apitools.gen import gen_client
 from apitools.gen import test_utils
@@ -32,7 +31,7 @@
         return f.read()
 
 
-class ClientGenCliTest(unittest2.TestCase):
+class ClientGenCliTest(unittest.TestCase):
 
     def testHelp_NotEnoughArguments(self):
         with self.assertRaisesRegexp(SystemExit, '0'):
diff --git a/apitools/gen/service_registry.py b/apitools/gen/service_registry.py
index e47b050..b79f0d1 100644
--- a/apitools/gen/service_registry.py
+++ b/apitools/gen/service_registry.py
@@ -218,6 +218,7 @@
             printer()
             printer('MESSAGES_MODULE = messages')
             printer('BASE_URL = {0!r}'.format(client_info.base_url))
+            printer('MTLS_BASE_URL = {0!r}'.format(client_info.mtls_base_url))
             printer()
             printer('_PACKAGE = {0!r}'.format(client_info.package))
             printer('_SCOPES = {0!r}'.format(
diff --git a/apitools/gen/test_utils.py b/apitools/gen/test_utils.py
index 484dcbc..e6b5373 100644
--- a/apitools/gen/test_utils.py
+++ b/apitools/gen/test_utils.py
@@ -20,12 +20,12 @@
 import shutil
 import sys
 import tempfile
+import unittest
 
 import six
-import unittest2
 
 
-SkipOnWindows = unittest2.skipIf(
+SkipOnWindows = unittest.skipIf(
     os.name == 'nt', 'Does not run on windows')
 
 
diff --git a/apitools/gen/util.py b/apitools/gen/util.py
index 680d84a..c2955a8 100644
--- a/apitools/gen/util.py
+++ b/apitools/gen/util.py
@@ -93,7 +93,7 @@
         name = re.sub('[^_A-Za-z0-9]', '_', name)
         if name[0].isdigit():
             name = '_%s' % name
-        while keyword.iskeyword(name):
+        while keyword.iskeyword(name) or name == 'exec':
             name = '%s_' % name
         # If we end up with __ as a prefix, we'll run afoul of python
         # field renaming, so we manually correct for it.
@@ -174,9 +174,21 @@
     return version.replace('.', '_')
 
 
-def _ComputePaths(package, version, discovery_doc):
-    full_path = urllib_parse.urljoin(
-        discovery_doc['rootUrl'], discovery_doc['servicePath'])
+def _ComputePaths(package, version, root_url, service_path):
+    """Compute the base url and base path.
+
+    Attributes:
+      package: name field of the discovery, i.e. 'storage' for storage service.
+      version: version of the service, i.e. 'v1'.
+      root_url: root url of the service, i.e. 'https://www.googleapis.com/'.
+      service_path: path of the service under the rool url, i.e. 'storage/v1/'.
+
+    Returns:
+      base url: string, base url of the service,
+        'https://www.googleapis.com/storage/v1/' for the storage service.
+      base path: string, common prefix of service endpoints after the base url.
+    """
+    full_path = urllib_parse.urljoin(root_url, service_path)
     api_path_component = '/'.join((package, version, ''))
     if api_path_component not in full_path:
         return full_path, ''
@@ -187,7 +199,7 @@
 class ClientInfo(collections.namedtuple('ClientInfo', (
         'package', 'scopes', 'version', 'client_id', 'client_secret',
         'user_agent', 'client_class_name', 'url_version', 'api_key',
-        'base_url', 'base_path'))):
+        'base_url', 'base_path', 'mtls_base_url'))):
 
     """Container for client-related info and names."""
 
@@ -201,7 +213,15 @@
         package = discovery_doc['name']
         url_version = discovery_doc['version']
         base_url, base_path = _ComputePaths(package, url_version,
-                                            discovery_doc)
+                                            discovery_doc['rootUrl'],
+                                            discovery_doc['servicePath'])
+
+        mtls_root_url = discovery_doc.get('mtlsRootUrl', '')
+        mtls_base_url = ''
+        if mtls_root_url:
+            mtls_base_url, _ = _ComputePaths(package, url_version,
+                                             mtls_root_url,
+                                             discovery_doc['servicePath'])
 
         client_info = {
             'package': package,
@@ -214,6 +234,7 @@
             'api_key': api_key,
             'base_url': base_url,
             'base_path': base_path,
+            'mtls_base_url': mtls_base_url,
         }
         client_class_name = '%s%s' % (
             names.ClassName(client_info['package']),
@@ -403,7 +424,8 @@
                 if isinstance(content, bytes):
                     content = content.decode('utf8')
                 discovery_doc = json.loads(content)
-                break
+                if discovery_doc:
+                    return discovery_doc
             except (urllib_error.HTTPError, urllib_error.URLError) as e:
                 logging.info(
                     'Attempting to fetch discovery doc again after "%s"', e)
@@ -412,4 +434,3 @@
         raise CommunicationError(
             'Could not find discovery doc at any of %s: %s' % (
                 discovery_urls, last_exception))
-    return discovery_doc
diff --git a/apitools/gen/util_test.py b/apitools/gen/util_test.py
index 7668b53..9682bf9 100644
--- a/apitools/gen/util_test.py
+++ b/apitools/gen/util_test.py
@@ -21,13 +21,13 @@
 import os
 import six.moves.urllib.request as urllib_request
 import tempfile
-import unittest2
+import unittest
 
 from apitools.gen import util
 from mock import patch
 
 
-class NormalizeVersionTest(unittest2.TestCase):
+class NormalizeVersionTest(unittest.TestCase):
 
     def testVersions(self):
         already_valid = 'v1'
@@ -36,7 +36,7 @@
         self.assertEqual('v0_1', util.NormalizeVersion(to_clean))
 
 
-class NamesTest(unittest2.TestCase):
+class NamesTest(unittest.TestCase):
 
     def testKeywords(self):
         names = util.Names([''])
@@ -81,7 +81,7 @@
         os.unlink(f.name)
 
 
-class GetURLContentTest(unittest2.TestCase):
+class GetURLContentTest(unittest.TestCase):
 
     def testUnspecifiedContentEncoding(self):
         data = 'regular non-gzipped content'
diff --git a/samples/bigquery_sample/bigquery_v2/bigquery_v2_client.py b/samples/bigquery_sample/bigquery_v2/bigquery_v2_client.py
index e6cf9c8..90552da 100644
--- a/samples/bigquery_sample/bigquery_v2/bigquery_v2_client.py
+++ b/samples/bigquery_sample/bigquery_v2/bigquery_v2_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://www.googleapis.com/bigquery/v2/'
+  MTLS_BASE_URL = u''
 
   _PACKAGE = u'bigquery'
   _SCOPES = [u'https://www.googleapis.com/auth/bigquery', u'https://www.googleapis.com/auth/bigquery.insertdata', u'https://www.googleapis.com/auth/cloud-platform', u'https://www.googleapis.com/auth/cloud-platform.read-only', u'https://www.googleapis.com/auth/devstorage.full_control', u'https://www.googleapis.com/auth/devstorage.read_only', u'https://www.googleapis.com/auth/devstorage.read_write']
diff --git a/samples/dns_sample/dns_v1/dns_v1_client.py b/samples/dns_sample/dns_v1/dns_v1_client.py
index ce3aff6..0666460 100644
--- a/samples/dns_sample/dns_v1/dns_v1_client.py
+++ b/samples/dns_sample/dns_v1/dns_v1_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://www.googleapis.com/dns/v1/'
+  MTLS_BASE_URL = u''
 
   _PACKAGE = u'dns'
   _SCOPES = [u'https://www.googleapis.com/auth/cloud-platform', u'https://www.googleapis.com/auth/cloud-platform.read-only', u'https://www.googleapis.com/auth/ndev.clouddns.readonly', u'https://www.googleapis.com/auth/ndev.clouddns.readwrite']
diff --git a/samples/dns_sample/gen_dns_client_test.py b/samples/dns_sample/gen_dns_client_test.py
index dff6812..862ddba 100644
--- a/samples/dns_sample/gen_dns_client_test.py
+++ b/samples/dns_sample/gen_dns_client_test.py
@@ -15,7 +15,8 @@
 
 """Test for generated sample module."""
 
-import unittest2
+import unittest
+
 import six
 
 from apitools.base.py import list_pager
@@ -25,7 +26,7 @@
 from samples.dns_sample.dns_v1 import dns_v1_messages
 
 
-class DnsGenClientSanityTest(unittest2.TestCase):
+class DnsGenClientSanityTest(unittest.TestCase):
 
     def testBaseUrl(self):
         self.assertEquals(u'https://www.googleapis.com/dns/v1/',
@@ -46,7 +47,7 @@
             'ResourceRecordSetsService']), inner_classes)
 
 
-class DnsGenClientTest(unittest2.TestCase):
+class DnsGenClientTest(unittest.TestCase):
 
     def setUp(self):
         self.mocked_dns_v1 = mock.Client(dns_v1_client.DnsV1)
diff --git a/samples/fusiontables_sample/fusiontables_v1/fusiontables_v1_client.py b/samples/fusiontables_sample/fusiontables_v1/fusiontables_v1_client.py
index f80fb3e..b7b6c43 100644
--- a/samples/fusiontables_sample/fusiontables_v1/fusiontables_v1_client.py
+++ b/samples/fusiontables_sample/fusiontables_v1/fusiontables_v1_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://www.googleapis.com/fusiontables/v1/'
+  MTLS_BASE_URL = u''
 
   _PACKAGE = u'fusiontables'
   _SCOPES = [u'https://www.googleapis.com/auth/fusiontables', u'https://www.googleapis.com/auth/fusiontables.readonly']
diff --git a/samples/iam_sample/iam_client_test.py b/samples/iam_sample/iam_client_test.py
index 39d25a4..017a2d0 100644
--- a/samples/iam_sample/iam_client_test.py
+++ b/samples/iam_sample/iam_client_test.py
@@ -15,7 +15,8 @@
 
 """Test for generated sample module."""
 
-import unittest2
+import unittest
+
 import six
 
 from apitools.base.py.testing import mock
@@ -24,7 +25,7 @@
 from samples.iam_sample.iam_v1 import iam_v1_messages  # nopep8
 
 
-class DnsGenClientSanityTest(unittest2.TestCase):
+class DnsGenClientSanityTest(unittest.TestCase):
 
     def testBaseUrl(self):
         self.assertEquals(u'https://iam.googleapis.com/',
@@ -46,7 +47,7 @@
             'RolesService']), inner_classes)
 
 
-class IamGenClientTest(unittest2.TestCase):
+class IamGenClientTest(unittest.TestCase):
 
     def setUp(self):
         self.mocked_iam_v1 = mock.Client(iam_v1_client.IamV1)
diff --git a/samples/iam_sample/iam_v1/iam_v1_client.py b/samples/iam_sample/iam_v1/iam_v1_client.py
index 9f333ef..ed9112e 100644
--- a/samples/iam_sample/iam_v1/iam_v1_client.py
+++ b/samples/iam_sample/iam_v1/iam_v1_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://iam.googleapis.com/'
+  MTLS_BASE_URL = u''
 
   _PACKAGE = u'iam'
   _SCOPES = [u'https://www.googleapis.com/auth/cloud-platform']
diff --git a/samples/servicemanagement_sample/messages_test.py b/samples/servicemanagement_sample/messages_test.py
index a62dbd7..5f56322 100644
--- a/samples/servicemanagement_sample/messages_test.py
+++ b/samples/servicemanagement_sample/messages_test.py
@@ -15,7 +15,7 @@
 
 """Test for generated servicemanagement messages module."""
 
-import unittest2
+import unittest
 
 from apitools.base.py import extra_types
 
@@ -23,7 +23,7 @@
     import servicemanagement_v1_messages as messages  # nopep8
 
 
-class MessagesTest(unittest2.TestCase):
+class MessagesTest(unittest.TestCase):
 
     def testInstantiateMessageWithAdditionalProperties(self):
         PROJECT_NAME = 'test-project'
diff --git a/samples/servicemanagement_sample/servicemanagement_v1/servicemanagement_v1_client.py b/samples/servicemanagement_sample/servicemanagement_v1/servicemanagement_v1_client.py
index a72936e..25823db 100644
--- a/samples/servicemanagement_sample/servicemanagement_v1/servicemanagement_v1_client.py
+++ b/samples/servicemanagement_sample/servicemanagement_v1/servicemanagement_v1_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://servicemanagement.googleapis.com/'
+  MTLS_BASE_URL = u''
 
   _PACKAGE = u'servicemanagement'
   _SCOPES = [u'https://www.googleapis.com/auth/cloud-platform', u'https://www.googleapis.com/auth/service.management']
diff --git a/samples/storage_sample/storage_v1.json b/samples/storage_sample/storage_v1.json
index 2636bda..cfd2748 100644
--- a/samples/storage_sample/storage_v1.json
+++ b/samples/storage_sample/storage_v1.json
@@ -21,6 +21,7 @@
  "baseUrl": "https://www.googleapis.com/storage/v1/",
  "basePath": "/storage/v1/",
  "rootUrl": "https://www.googleapis.com/",
+ "mtlsRootUrl": "https://www.mtls.googleapis.com/",
  "servicePath": "storage/v1/",
  "batchPath": "batch/storage/v1",
  "parameters": {
diff --git a/samples/storage_sample/storage_v1/storage_v1_client.py b/samples/storage_sample/storage_v1/storage_v1_client.py
index 38ceab9..4a8414a 100644
--- a/samples/storage_sample/storage_v1/storage_v1_client.py
+++ b/samples/storage_sample/storage_v1/storage_v1_client.py
@@ -9,6 +9,7 @@
 
   MESSAGES_MODULE = messages
   BASE_URL = u'https://www.googleapis.com/storage/v1/'
+  MTLS_BASE_URL = u'https://www.mtls.googleapis.com/storage/v1/'
 
   _PACKAGE = u'storage'
   _SCOPES = [u'https://www.googleapis.com/auth/cloud-platform', u'https://www.googleapis.com/auth/cloud-platform.read-only', u'https://www.googleapis.com/auth/devstorage.full_control', u'https://www.googleapis.com/auth/devstorage.read_only', u'https://www.googleapis.com/auth/devstorage.read_write']
diff --git a/samples/uptodate_check_test.py b/samples/uptodate_check_test.py
index 3871695..8ca258e 100644
--- a/samples/uptodate_check_test.py
+++ b/samples/uptodate_check_test.py
@@ -14,9 +14,9 @@
 
 import os
 import difflib
+import unittest
 
 import six
-import unittest2
 
 from apitools.gen import gen_client
 from apitools.gen import test_utils
@@ -31,7 +31,7 @@
         return f.read()
 
 
-class ClientGenCliTest(unittest2.TestCase):
+class ClientGenCliTest(unittest.TestCase):
 
     def AssertDiffEqual(self, expected, actual):
         """Like unittest.assertEqual with a diff in the exception message."""
diff --git a/setup.py b/setup.py
index fbf81b1..f6e26a2 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,6 @@
 ]
 
 TESTING_PACKAGES = [
-    'unittest2>=0.5.1',
     'mock>=1.0.1',
 ]
 
@@ -49,7 +48,7 @@
 
 py_version = platform.python_version()
 
-_APITOOLS_VERSION = '0.5.30'
+_APITOOLS_VERSION = '0.5.31'
 
 with open('README.rst') as fileobj:
     README = fileobj.read()
@@ -62,6 +61,7 @@
     url='http://github.com/google/apitools',
     author='Craig Citro',
     author_email='craigcitro@google.com',
+    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
     # Contained modules and scripts.
     packages=setuptools.find_packages(include=['apitools']),
     entry_points={'console_scripts': CONSOLE_SCRIPTS},
@@ -88,6 +88,10 @@
     # PyPI package information.
     classifiers=[
         'License :: OSI Approved :: Apache Software License',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.5',
         'Topic :: Software Development :: Libraries',
         'Topic :: Software Development :: Libraries :: Python Modules',
         ],
diff --git a/tox.ini b/tox.ini
index aaa22e0..09d2afc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,6 @@
 [tox]
 envlist =
-    py26-oauth2client4
     py27-oauth2client{1,2,3,4}
-    py33-oauth2client41
-    py34-oauth2client41
     py35-oauth2client{1,2,3,4}
 
 [testenv]
@@ -28,7 +25,6 @@
 deps =
     pycodestyle==2.4.0
     pylint
-    unittest2
 
 [testenv:cover]
 basepython =
@@ -39,7 +35,6 @@
     python-gflags
     mock
     nose
-    unittest2
     coverage
     nosexcover
 
@@ -58,7 +53,6 @@
 deps =
     mock
     nose
-    unittest2
     coverage
 commands =
     coverage run --branch -p samples/storage_sample/downloads_test.py