Move run_pylint to gcp-devrel-py-tools (#153)

diff --git a/.gitignore b/.gitignore
index 2c7b722..1f65cf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,7 @@
 
 # PyCharm configuration:
 .idea
+
+# Generated files
+pylintrc
+pylintrc.test
diff --git a/pylint.config.py b/pylint.config.py
new file mode 100644
index 0000000..d63f337
--- /dev/null
+++ b/pylint.config.py
@@ -0,0 +1,71 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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.
+
+"""This module is used to config gcp-devrel-py-tools run-pylint."""
+
+import copy
+
+library_additions = {
+    'MESSAGES CONTROL': {
+        'disable': [
+            'I',
+            'import-error',
+            'no-member',
+            'protected-access',
+            'redefined-variable-type',
+            'similarities',
+            'no-else-return',
+        ],
+    },
+}
+
+library_replacements = {
+    'MASTER': {
+        'ignore': ['CVS', '.git', '.cache', '.tox', '.nox'],
+        'load-plugins': 'pylint.extensions.check_docs',
+    },
+    'REPORTS': {
+        'reports': 'no',
+    },
+    'BASIC': {
+        'method-rgx': '[a-z_][a-z0-9_]{2,40}$',
+        'function-rgx': '[a-z_][a-z0-9_]{2,40}$',
+    },
+    'TYPECHECK': {
+        'ignored-modules': ['six', 'google.protobuf'],
+    },
+    'DESIGN': {
+        'min-public-methods': '0',
+        'max-args': '10',
+        'max-attributes': '15',
+    },
+}
+
+test_additions = copy.deepcopy(library_additions)
+test_additions['MESSAGES CONTROL']['disable'].extend([
+    'missing-docstring',
+    'no-self-use',
+    'redefined-outer-name',
+    'unused-argument',
+    'no-name-in-module',
+])
+test_replacements = copy.deepcopy(library_replacements)
+test_replacements.setdefault('BASIC', {})
+test_replacements['BASIC'].update({
+    'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'],
+    'method-rgx': '[a-z_][a-z0-9_]{2,80}$',
+    'function-rgx': '[a-z_][a-z0-9_]{2,80}$',
+})
+
+ignored_files = ()
diff --git a/scripts/.gitignore b/scripts/.gitignore
deleted file mode 100644
index 3596d32..0000000
--- a/scripts/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Generated files
-pylintrc
-pylintrc.test
diff --git a/scripts/run_pylint.py b/scripts/run_pylint.py
deleted file mode 100644
index c33a53e..0000000
--- a/scripts/run_pylint.py
+++ /dev/null
@@ -1,260 +0,0 @@
-# Copyright 2016 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License 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.
-
-"""This script runs Pylint on the specified source.
-
-Before running Pylint, it generates a Pylint configuration on
-the fly based on programmatic defaults.
-"""
-
-from __future__ import print_function
-
-import argparse
-import collections
-import copy
-import io
-import os
-import subprocess
-import sys
-
-import six
-
-
-_SCRIPTS_DIR = os.path.abspath(os.path.dirname(__file__))
-PRODUCTION_RC = os.path.join(_SCRIPTS_DIR, 'pylintrc')
-TEST_RC = os.path.join(_SCRIPTS_DIR, 'pylintrc.test')
-
-_PRODUCTION_RC_ADDITIONS = {
-    'MESSAGES CONTROL': {
-        'disable': [
-            'I',
-            'import-error',
-            'no-member',
-            'protected-access',
-            'redefined-variable-type',
-            'similarities',
-            'no-else-return',
-        ],
-    },
-}
-_PRODUCTION_RC_REPLACEMENTS = {
-    'MASTER': {
-        'ignore': ['CVS', '.git', '.cache', '.tox', '.nox'],
-        'load-plugins': 'pylint.extensions.check_docs',
-    },
-    'REPORTS': {
-        'reports': 'no',
-    },
-    'BASIC': {
-        'method-rgx': '[a-z_][a-z0-9_]{2,40}$',
-        'function-rgx': '[a-z_][a-z0-9_]{2,40}$',
-    },
-    'TYPECHECK': {
-        'ignored-modules': ['six', 'google.protobuf'],
-    },
-    'DESIGN': {
-        'min-public-methods': '0',
-        'max-args': '10',
-        'max-attributes': '15',
-    },
-}
-_TEST_RC_ADDITIONS = copy.deepcopy(_PRODUCTION_RC_ADDITIONS)
-_TEST_RC_ADDITIONS['MESSAGES CONTROL']['disable'].extend([
-    'missing-docstring',
-    'no-self-use',
-    'redefined-outer-name',
-    'unused-argument',
-    'no-name-in-module',
-])
-_TEST_RC_REPLACEMENTS = copy.deepcopy(_PRODUCTION_RC_REPLACEMENTS)
-_TEST_RC_REPLACEMENTS.setdefault('BASIC', {})
-_TEST_RC_REPLACEMENTS['BASIC'].update({
-    'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'],
-    'method-rgx': '[a-z_][a-z0-9_]{2,80}$',
-    'function-rgx': '[a-z_][a-z0-9_]{2,80}$',
-})
-IGNORED_FILES = ()
-
-_ERROR_TEMPLATE = 'Pylint failed on {} with status {:d}.'
-_LINT_FILESET_MSG = (
-    'Keyword arguments rc_filename and description are both '
-    'required. No other keyword arguments are allowed.')
-
-
-def get_default_config():
-    """Get the default Pylint configuration.
-
-    .. note::
-
-        The output of this function varies based on the current version of
-        Pylint installed.
-
-    Returns:
-        str: The default Pylint configuration.
-    """
-    # Swallow STDERR if it says
-    # "No config file found, using default configuration"
-    result = subprocess.check_output(['pylint', '--generate-rcfile'],
-                                     stderr=subprocess.PIPE)
-    # On Python 3, this returns bytes (from STDOUT), so we
-    # convert to a string.
-    return result.decode('utf-8')
-
-
-def read_config(contents):
-    """Reads pylintrc config into native ConfigParser object.
-
-    Args:
-        contents (str): The contents of the file containing the INI config.
-
-    Returns
-        ConfigParser.ConfigParser: The parsed configuration.
-    """
-    file_obj = io.StringIO(contents)
-    config = six.moves.configparser.ConfigParser()
-    config.readfp(file_obj)
-    return config
-
-
-def _transform_opt(opt_val):
-    """Transform a config option value to a string.
-
-    If already a string, do nothing. If an iterable, then
-    combine into a string by joining on ",".
-
-    Args:
-        opt_val (Union[str, list]): A config option's value.
-
-    Returns:
-        str: The option value converted to a string.
-    """
-    if isinstance(opt_val, (list, tuple)):
-        return ','.join(opt_val)
-    else:
-        return opt_val
-
-
-def lint_fileset(*dirnames, **kwargs):
-    """Lints a group of files using a given rcfile.
-
-    Keyword arguments are
-
-    * ``rc_filename`` (``str``): The name of the Pylint config RC file.
-    * ``description`` (``str``): A description of the files and configuration
-                                 currently being run.
-
-    Args:
-        dirnames (tuple): Directories to run Pylint in.
-        kwargs: The keyword arguments. The only keyword arguments
-            are ``rc_filename`` and ``description`` and both
-            are required.
-
-    Raises:
-        KeyError: If the wrong keyword arguments are used.
-    """
-    try:
-        rc_filename = kwargs['rc_filename']
-        description = kwargs['description']
-        if len(kwargs) != 2:
-            raise KeyError
-    except KeyError:
-        raise KeyError(_LINT_FILESET_MSG)
-
-    pylint_shell_command = ['pylint', '--rcfile', rc_filename]
-    pylint_shell_command.extend(dirnames)
-    status_code = subprocess.call(pylint_shell_command)
-    if status_code != 0:
-        error_message = _ERROR_TEMPLATE.format(description, status_code)
-        print(error_message, file=sys.stderr)
-        sys.exit(status_code)
-
-
-def make_rc(base_cfg, target_filename,
-            additions=None, replacements=None):
-    """Combines a base rc and additions into single file.
-
-    Args:
-        base_cfg (ConfigParser.ConfigParser): The configuration we are
-            merging into.
-        target_filename (str): The filename where the new configuration
-            will be saved.
-        additions (dict): (Optional) The values added to the configuration.
-        replacements (dict): (Optional) The wholesale replacements for
-            the new configuration.
-
-    Raises:
-        KeyError: if one of the additions or replacements does not
-            already exist in the current config.
-    """
-    # Set-up the mutable default values.
-    if additions is None:
-        additions = {}
-    if replacements is None:
-        replacements = {}
-
-    # Create fresh config, which must extend the base one.
-    new_cfg = six.moves.configparser.ConfigParser()
-    # pylint: disable=protected-access
-    new_cfg._sections = copy.deepcopy(base_cfg._sections)
-    new_sections = new_cfg._sections
-    # pylint: enable=protected-access
-
-    for section, opts in additions.items():
-        curr_section = new_sections.setdefault(
-            section, collections.OrderedDict())
-        for opt, opt_val in opts.items():
-            curr_val = curr_section.get(opt)
-            if curr_val is None:
-                raise KeyError('Expected to be adding to existing option.')
-            curr_val = curr_val.rstrip(',')
-            opt_val = _transform_opt(opt_val)
-            curr_section[opt] = '%s, %s' % (curr_val, opt_val)
-
-    for section, opts in replacements.items():
-        curr_section = new_sections.setdefault(
-            section, collections.OrderedDict())
-        for opt, opt_val in opts.items():
-            curr_val = curr_section.get(opt)
-            if curr_val is None:
-                raise KeyError('Expected to be replacing existing option.')
-            opt_val = _transform_opt(opt_val)
-            curr_section[opt] = '%s' % (opt_val,)
-
-    with open(target_filename, 'w') as file_obj:
-        new_cfg.write(file_obj)
-
-
-def main():
-    """Script entry point. Lints both sets of files."""
-    parser = argparse.ArgumentParser()
-    parser.add_argument('--library-filesets', nargs='+', default=[])
-    parser.add_argument('--test-filesets', nargs='+', default=[])
-
-    args = parser.parse_args()
-
-    default_config = read_config(get_default_config())
-    make_rc(default_config, PRODUCTION_RC,
-            additions=_PRODUCTION_RC_ADDITIONS,
-            replacements=_PRODUCTION_RC_REPLACEMENTS)
-    make_rc(default_config, TEST_RC,
-            additions=_TEST_RC_ADDITIONS,
-            replacements=_TEST_RC_REPLACEMENTS)
-
-    lint_fileset(*args.library_filesets, rc_filename=PRODUCTION_RC,
-                 description='Library')
-    lint_fileset(*args.test_filesets, rc_filename=TEST_RC,
-                 description='Test')
-
-if __name__ == '__main__':
-    main()
diff --git a/scripts/travis.sh b/scripts/travis.sh
index 5acb4c2..28bf5fa 100755
--- a/scripts/travis.sh
+++ b/scripts/travis.sh
@@ -40,17 +40,3 @@
 # Run tox.
 echo "Running tox..."
 tox
-
-# Run tox for sub-packages.
-if [[ $TOXENV != "docs"  && -z $SYSTEM_TEST ]]; then
-    echo "Running tox for google_auth_httplib2..."
-    cd additional_packages/google_auth_httplib2
-    # --workdir is specified to avoid path names being too long, which
-    # causes subprocess calls to hit the execve character limit.
-    # See https://github.com/pypa/virtualenv/issues/596
-    tox --workdir ~/.tox-httplib2
-    cd $ROOT
-    echo "Running tox for google_auth_oauthlib..."
-    cd additional_packages/google_auth_oauthlib
-    tox --workdir  ~/.tox-oauthlib
-fi
diff --git a/tests/transport/test_requests.py b/tests/transport/test_requests.py
index 4be4668..ac2a024 100644
--- a/tests/transport/test_requests.py
+++ b/tests/transport/test_requests.py
@@ -49,6 +49,7 @@
 
 class MockAdapter(requests.adapters.BaseAdapter):
     def __init__(self, responses, headers=None):
+        super(MockAdapter, self).__init__()
         self.responses = responses
         self.requests = []
         self.headers = headers or {}
@@ -57,6 +58,11 @@
         self.requests.append(request)
         return self.responses.pop(0)
 
+    def close(self):  # pragma: NO COVER
+        # pylint wants this to be here because it's abstract in the base
+        # class, but requests never actually calls it.
+        return
+
 
 def make_response(status=http_client.OK, data=None):
     response = requests.Response()
diff --git a/tox.ini b/tox.ini
index 0a0d806..ddc6430 100644
--- a/tox.ini
+++ b/tox.ini
@@ -77,7 +77,8 @@
     --import-order-style=google \
     --application-import-names="google,tests,system_tests" \
     google tests
-  python {toxinidir}/scripts/run_pylint.py \
+  gcp-devrel-py-tools run-pylint \
+    --config pylint.config.py \
     --library-filesets google \
     --test-filesets tests system_tests
 deps =
@@ -85,3 +86,4 @@
   flake8-import-order
   pylint
   docutils
+  gcp-devrel-py-tools