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