blob: c33a53e1e96b5566456f623c4fe18d374b3fe1e6 [file] [log] [blame]
Danny Hermes97791812016-11-01 12:43:01 -07001# Copyright 2016 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""This script runs Pylint on the specified source.
16
17Before running Pylint, it generates a Pylint configuration on
18the fly based on programmatic defaults.
19"""
20
21from __future__ import print_function
22
Jon Wayne Parrott06a27e82017-03-22 12:59:41 -070023import argparse
Danny Hermes97791812016-11-01 12:43:01 -070024import collections
25import copy
26import io
27import os
28import subprocess
29import sys
30
31import six
32
33
34_SCRIPTS_DIR = os.path.abspath(os.path.dirname(__file__))
35PRODUCTION_RC = os.path.join(_SCRIPTS_DIR, 'pylintrc')
36TEST_RC = os.path.join(_SCRIPTS_DIR, 'pylintrc.test')
37
38_PRODUCTION_RC_ADDITIONS = {
39 'MESSAGES CONTROL': {
40 'disable': [
41 'I',
42 'import-error',
43 'no-member',
44 'protected-access',
45 'redefined-variable-type',
46 'similarities',
Jon Wayne Parrott20e6e582016-12-19 10:21:23 -080047 'no-else-return',
Danny Hermes97791812016-11-01 12:43:01 -070048 ],
49 },
50}
51_PRODUCTION_RC_REPLACEMENTS = {
52 'MASTER': {
53 'ignore': ['CVS', '.git', '.cache', '.tox', '.nox'],
54 'load-plugins': 'pylint.extensions.check_docs',
55 },
56 'REPORTS': {
57 'reports': 'no',
58 },
59 'BASIC': {
60 'method-rgx': '[a-z_][a-z0-9_]{2,40}$',
61 'function-rgx': '[a-z_][a-z0-9_]{2,40}$',
62 },
63 'TYPECHECK': {
64 'ignored-modules': ['six', 'google.protobuf'],
65 },
66 'DESIGN': {
67 'min-public-methods': '0',
68 'max-args': '10',
69 'max-attributes': '15',
70 },
71}
72_TEST_RC_ADDITIONS = copy.deepcopy(_PRODUCTION_RC_ADDITIONS)
73_TEST_RC_ADDITIONS['MESSAGES CONTROL']['disable'].extend([
74 'missing-docstring',
75 'no-self-use',
76 'redefined-outer-name',
77 'unused-argument',
Jon Wayne Parrottb9897dc2016-11-02 20:31:14 -070078 'no-name-in-module',
Danny Hermes97791812016-11-01 12:43:01 -070079])
80_TEST_RC_REPLACEMENTS = copy.deepcopy(_PRODUCTION_RC_REPLACEMENTS)
81_TEST_RC_REPLACEMENTS.setdefault('BASIC', {})
82_TEST_RC_REPLACEMENTS['BASIC'].update({
Jon Wayne Parrottb9897dc2016-11-02 20:31:14 -070083 'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'],
Danny Hermes97791812016-11-01 12:43:01 -070084 'method-rgx': '[a-z_][a-z0-9_]{2,80}$',
85 'function-rgx': '[a-z_][a-z0-9_]{2,80}$',
86})
87IGNORED_FILES = ()
88
89_ERROR_TEMPLATE = 'Pylint failed on {} with status {:d}.'
90_LINT_FILESET_MSG = (
91 'Keyword arguments rc_filename and description are both '
92 'required. No other keyword arguments are allowed.')
93
94
95def get_default_config():
96 """Get the default Pylint configuration.
97
98 .. note::
99
100 The output of this function varies based on the current version of
101 Pylint installed.
102
103 Returns:
104 str: The default Pylint configuration.
105 """
106 # Swallow STDERR if it says
107 # "No config file found, using default configuration"
108 result = subprocess.check_output(['pylint', '--generate-rcfile'],
109 stderr=subprocess.PIPE)
110 # On Python 3, this returns bytes (from STDOUT), so we
111 # convert to a string.
112 return result.decode('utf-8')
113
114
115def read_config(contents):
116 """Reads pylintrc config into native ConfigParser object.
117
118 Args:
119 contents (str): The contents of the file containing the INI config.
120
121 Returns
122 ConfigParser.ConfigParser: The parsed configuration.
123 """
124 file_obj = io.StringIO(contents)
125 config = six.moves.configparser.ConfigParser()
126 config.readfp(file_obj)
127 return config
128
129
130def _transform_opt(opt_val):
131 """Transform a config option value to a string.
132
133 If already a string, do nothing. If an iterable, then
134 combine into a string by joining on ",".
135
136 Args:
137 opt_val (Union[str, list]): A config option's value.
138
139 Returns:
140 str: The option value converted to a string.
141 """
142 if isinstance(opt_val, (list, tuple)):
143 return ','.join(opt_val)
144 else:
145 return opt_val
146
147
148def lint_fileset(*dirnames, **kwargs):
149 """Lints a group of files using a given rcfile.
150
151 Keyword arguments are
152
153 * ``rc_filename`` (``str``): The name of the Pylint config RC file.
154 * ``description`` (``str``): A description of the files and configuration
155 currently being run.
156
157 Args:
158 dirnames (tuple): Directories to run Pylint in.
159 kwargs: The keyword arguments. The only keyword arguments
160 are ``rc_filename`` and ``description`` and both
161 are required.
162
163 Raises:
164 KeyError: If the wrong keyword arguments are used.
165 """
166 try:
167 rc_filename = kwargs['rc_filename']
168 description = kwargs['description']
169 if len(kwargs) != 2:
170 raise KeyError
171 except KeyError:
172 raise KeyError(_LINT_FILESET_MSG)
173
174 pylint_shell_command = ['pylint', '--rcfile', rc_filename]
175 pylint_shell_command.extend(dirnames)
176 status_code = subprocess.call(pylint_shell_command)
177 if status_code != 0:
178 error_message = _ERROR_TEMPLATE.format(description, status_code)
179 print(error_message, file=sys.stderr)
180 sys.exit(status_code)
181
182
183def make_rc(base_cfg, target_filename,
184 additions=None, replacements=None):
185 """Combines a base rc and additions into single file.
186
187 Args:
188 base_cfg (ConfigParser.ConfigParser): The configuration we are
189 merging into.
190 target_filename (str): The filename where the new configuration
191 will be saved.
192 additions (dict): (Optional) The values added to the configuration.
193 replacements (dict): (Optional) The wholesale replacements for
194 the new configuration.
195
196 Raises:
197 KeyError: if one of the additions or replacements does not
198 already exist in the current config.
199 """
200 # Set-up the mutable default values.
201 if additions is None:
202 additions = {}
203 if replacements is None:
204 replacements = {}
205
206 # Create fresh config, which must extend the base one.
207 new_cfg = six.moves.configparser.ConfigParser()
208 # pylint: disable=protected-access
209 new_cfg._sections = copy.deepcopy(base_cfg._sections)
210 new_sections = new_cfg._sections
211 # pylint: enable=protected-access
212
213 for section, opts in additions.items():
214 curr_section = new_sections.setdefault(
215 section, collections.OrderedDict())
216 for opt, opt_val in opts.items():
217 curr_val = curr_section.get(opt)
218 if curr_val is None:
219 raise KeyError('Expected to be adding to existing option.')
220 curr_val = curr_val.rstrip(',')
221 opt_val = _transform_opt(opt_val)
222 curr_section[opt] = '%s, %s' % (curr_val, opt_val)
223
224 for section, opts in replacements.items():
225 curr_section = new_sections.setdefault(
226 section, collections.OrderedDict())
227 for opt, opt_val in opts.items():
228 curr_val = curr_section.get(opt)
229 if curr_val is None:
230 raise KeyError('Expected to be replacing existing option.')
231 opt_val = _transform_opt(opt_val)
232 curr_section[opt] = '%s' % (opt_val,)
233
234 with open(target_filename, 'w') as file_obj:
235 new_cfg.write(file_obj)
236
237
238def main():
239 """Script entry point. Lints both sets of files."""
Jon Wayne Parrott06a27e82017-03-22 12:59:41 -0700240 parser = argparse.ArgumentParser()
241 parser.add_argument('--library-filesets', nargs='+', default=[])
242 parser.add_argument('--test-filesets', nargs='+', default=[])
243
244 args = parser.parse_args()
245
Danny Hermes97791812016-11-01 12:43:01 -0700246 default_config = read_config(get_default_config())
247 make_rc(default_config, PRODUCTION_RC,
248 additions=_PRODUCTION_RC_ADDITIONS,
249 replacements=_PRODUCTION_RC_REPLACEMENTS)
250 make_rc(default_config, TEST_RC,
251 additions=_TEST_RC_ADDITIONS,
252 replacements=_TEST_RC_REPLACEMENTS)
Danny Hermes97791812016-11-01 12:43:01 -0700253
Jon Wayne Parrott06a27e82017-03-22 12:59:41 -0700254 lint_fileset(*args.library_filesets, rc_filename=PRODUCTION_RC,
255 description='Library')
256 lint_fileset(*args.test_filesets, rc_filename=TEST_RC,
257 description='Test')
Danny Hermes97791812016-11-01 12:43:01 -0700258
259if __name__ == '__main__':
260 main()