blob: e9ac7ec779d52b894b780dd4e1e55219b0e48a12 [file] [log] [blame]
# 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 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."""
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('google', rc_filename=PRODUCTION_RC,
description='Library')
lint_fileset('tests', 'system_tests', rc_filename=TEST_RC,
description='Test')
if __name__ == '__main__':
main()