• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Wrapper to run pylint with the right settings."""
17
18import argparse
19import errno
20import os
21import sys
22import subprocess
23from typing import Dict, List, Optional, Set
24
25
26# This script is run by repohooks users.
27# See README.md for what version we may require.
28assert (sys.version_info.major, sys.version_info.minor) >= (3, 6), (
29    f'Python 3.6 or newer is required; found {sys.version}')
30
31
32DEFAULT_PYLINTRC_PATH = os.path.join(
33    os.path.dirname(os.path.realpath(__file__)), 'pylintrc')
34
35
36def run_lint(pylint: str, unknown: Optional[List[str]],
37             files: Optional[List[str]], init_hook: str,
38             pylintrc: Optional[str] = None) -> bool:
39    """Run lint command.
40
41    Upon error the stdout from pylint will be dumped to stdout and
42    False will be returned.
43    """
44    cmd = [pylint]
45
46    if not files:
47        # No files to analyze for this pylintrc file.
48        return True
49
50    if pylintrc:
51        cmd += ['--rcfile', pylintrc]
52
53    files.sort()
54    cmd += unknown + files
55
56    if init_hook:
57        cmd += ['--init-hook', init_hook]
58
59    try:
60        result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True,
61                                check=False)
62    except OSError as e:
63        if e.errno == errno.ENOENT:
64            print(f'{__file__}: unable to run `{cmd[0]}`: {e}',
65                  file=sys.stderr)
66            print(f'{__file__}: Try installing pylint: sudo apt-get install '
67                  f'{os.path.basename(cmd[0])}', file=sys.stderr)
68            return False
69
70        raise
71
72    if result.returncode:
73        print(f'{__file__}: Using pylintrc: {pylintrc}')
74        print(result.stdout)
75        return False
76
77    return True
78
79
80def find_parent_dirs_with_pylintrc(leafdir: str,
81                                   pylintrc_map: Dict[str, Set[str]]) -> None:
82    """Find all dirs containing a pylintrc between root dir and leafdir."""
83
84    # Find all pylintrc files, store the path. The path must end with '/'
85    # to make sure that string compare can be used to compare with full
86    # path to python files later.
87
88    rootdir = os.path.abspath(".") + os.sep
89    key = os.path.abspath(leafdir) + os.sep
90
91    if not key.startswith(rootdir):
92        sys.exit(f'{__file__}: The search directory {key} is outside the '
93                 f'repo dir {rootdir}')
94
95    while rootdir != key:
96        # This subdirectory has already been handled, skip it.
97        if key in pylintrc_map:
98            break
99
100        if os.path.exists(os.path.join(key, 'pylintrc')):
101            pylintrc_map.setdefault(key, set())
102            break
103
104        # Go up one directory.
105        key = os.path.abspath(os.path.join(key, os.pardir)) + os.sep
106
107
108def map_pyfiles_to_pylintrc(files: List[str]) -> Dict[str, Set[str]]:
109    """ Map all python files to a pylintrc file.
110
111    Generate dictionary with pylintrc-file dirnames (including trailing /)
112    as key containing sets with corresponding python files.
113    """
114
115    pylintrc_map = {}
116    # We assume pylint is running in the top directory of the project,
117    # so load the pylintrc file from there if it is available.
118    pylintrc = os.path.abspath('pylintrc')
119    if not os.path.exists(pylintrc):
120        pylintrc = DEFAULT_PYLINTRC_PATH
121        # If we pass a non-existent rcfile to pylint, it'll happily ignore
122        # it.
123        assert os.path.exists(pylintrc), f'Could not find {pylintrc}'
124    # Always add top directory, either there is a pylintrc or fallback to
125    # default.
126    key = os.path.abspath('.') + os.sep
127    pylintrc_map[key] = set()
128
129    search_dirs = {os.path.dirname(x) for x in files}
130    for search_dir in search_dirs:
131        find_parent_dirs_with_pylintrc(search_dir, pylintrc_map)
132
133    # List of directories where pylintrc files are stored, most
134    # specific path first.
135    rc_dir_names = sorted(pylintrc_map, reverse=True)
136    # Map all python files to a pylintrc file.
137    for f in files:
138        f_full = os.path.abspath(f)
139        for rc_dir in rc_dir_names:
140            # The pylintrc map keys always have trailing /.
141            if f_full.startswith(rc_dir):
142                pylintrc_map[rc_dir].add(f)
143                break
144        else:
145            sys.exit(f'{__file__}: Failed to map file {f} to a pylintrc file.')
146
147    return pylintrc_map
148
149
150def get_parser():
151    """Return a command line parser."""
152    parser = argparse.ArgumentParser(description=__doc__)
153    parser.add_argument('--init-hook', help='Init hook commands to run.')
154    parser.add_argument('--executable-path', default='pylint',
155                        help='The path of the pylint executable.')
156    parser.add_argument('--no-rcfile', dest='use_default_conf',
157                        help='Specify to use the executable\'s default '
158                        'configuration.',
159                        action='store_true')
160    parser.add_argument('files', nargs='+')
161    return parser
162
163
164def main(argv):
165    """The main entry."""
166    parser = get_parser()
167    opts, unknown = parser.parse_known_args(argv)
168    ret = 0
169
170    pylint = opts.executable_path
171    if not opts.use_default_conf:
172        pylintrc_map = map_pyfiles_to_pylintrc(opts.files)
173        first = True
174        for rc_dir, files in sorted(pylintrc_map.items()):
175            pylintrc = os.path.join(rc_dir, 'pylintrc')
176            if first:
177                first = False
178                assert os.path.abspath(rc_dir) == os.path.abspath('.'), (
179                    f'{__file__}: pylintrc in top dir not first in list')
180                if not os.path.exists(pylintrc):
181                    pylintrc = DEFAULT_PYLINTRC_PATH
182            if not run_lint(pylint, unknown, sorted(files),
183                            opts.init_hook, pylintrc):
184                ret = 1
185    # Not using rc files, pylint default behaviour.
186    elif not run_lint(pylint, unknown, sorted(opts.files), opts.init_hook):
187        ret = 1
188
189    return ret
190
191
192if __name__ == '__main__':
193    sys.exit(main(sys.argv[1:]))
194