1#!/usr/bin/env python3
2
3# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""
8Fix some flake8 errors by rewriting files for which flake8 emitted
9an error/warning. Usage (from the root dir):
10
11    $ python3 -m flake8 --exit-zero | python3 scripts/fix_flake8.py
12"""
13
14import sys
15import tempfile
16import shutil
17from collections import defaultdict
18from collections import namedtuple
19from pprint import pprint as pp  # NOQA
20
21
22ntentry = namedtuple('ntentry', 'msg, issue, lineno, pos, descr')
23
24
25# =====================================================================
26# utils
27# =====================================================================
28
29
30def enter_pdb():
31    from pdb import set_trace as st  # trick GIT commit hook
32    sys.stdin = open('/dev/tty')
33    st()
34
35
36def read_lines(fname):
37    with open(fname, 'rt') as f:
38        return f.readlines()
39
40
41def read_line(fname, lineno):
42    with open(fname, 'rt') as f:
43        for i, line in enumerate(f, 1):
44            if i == lineno:
45                return line
46    raise ValueError("lineno too big")
47
48
49def write_file(fname, newlines):
50    with tempfile.NamedTemporaryFile('wt', delete=False) as f:
51        for line in newlines:
52            f.write(line)
53    shutil.move(f.name, fname)
54
55
56# =====================================================================
57# handlers
58# =====================================================================
59
60
61def handle_f401(fname, lineno):
62    """ This is 'module imported but not used'
63    Able to handle this case:
64        import os
65
66    ...but not this:
67        from os import listdir
68    """
69    line = read_line(fname, lineno).strip()
70    if line.startswith('import '):
71        return True
72    else:
73        mods = line.partition(' import ')[2]  # everything after import
74        return ',' not in mods
75
76
77# =====================================================================
78# converters
79# =====================================================================
80
81
82def remove_lines(fname, entries):
83    """Check if we should remove lines, then do it.
84    Return the numner of lines removed.
85    """
86    to_remove = []
87    for entry in entries:
88        msg, issue, lineno, pos, descr = entry
89        # 'module imported but not used'
90        if issue == 'F401' and handle_f401(fname, lineno):
91            to_remove.append(lineno)
92        # 'blank line(s) at end of file'
93        elif issue == 'W391':
94            lines = read_lines(fname)
95            i = len(lines) - 1
96            while lines[i] == '\n':
97                to_remove.append(i + 1)
98                i -= 1
99        # 'too many blank lines'
100        elif issue == 'E303':
101            howmany = descr.replace('(', '').replace(')', '')
102            howmany = int(howmany[-1])
103            for x in range(lineno - howmany, lineno):
104                to_remove.append(x)
105
106    if to_remove:
107        newlines = []
108        for i, line in enumerate(read_lines(fname), 1):
109            if i not in to_remove:
110                newlines.append(line)
111        print("removing line(s) from %s" % fname)
112        write_file(fname, newlines)
113
114    return len(to_remove)
115
116
117def add_lines(fname, entries):
118    """Check if we should remove lines, then do it.
119    Return the numner of lines removed.
120    """
121    EXPECTED_BLANK_LINES = (
122        'E302',  # 'expected 2 blank limes, found 1'
123        'E305')  # ìexpected 2 blank lines after class or fun definition'
124    to_add = {}
125    for entry in entries:
126        msg, issue, lineno, pos, descr = entry
127        if issue in EXPECTED_BLANK_LINES:
128            howmany = 2 if descr.endswith('0') else 1
129            to_add[lineno] = howmany
130
131    if to_add:
132        newlines = []
133        for i, line in enumerate(read_lines(fname), 1):
134            if i in to_add:
135                newlines.append('\n' * to_add[i])
136            newlines.append(line)
137        print("adding line(s) to %s" % fname)
138        write_file(fname, newlines)
139
140    return len(to_add)
141
142
143# =====================================================================
144# main
145# =====================================================================
146
147
148def build_table():
149    table = defaultdict(list)
150    for line in sys.stdin:
151        line = line.strip()
152        if not line:
153            break
154        fields = line.split(':')
155        fname, lineno, pos = fields[:3]
156        issue = fields[3].split()[0]
157        descr = fields[3].strip().partition(' ')[2]
158        lineno, pos = int(lineno), int(pos)
159        table[fname].append(ntentry(line, issue, lineno, pos, descr))
160    return table
161
162
163def main():
164    table = build_table()
165
166    # remove lines (unused imports)
167    removed = 0
168    for fname, entries in table.items():
169        removed += remove_lines(fname, entries)
170    if removed:
171        print("%s lines were removed from some file(s); please re-run" %
172              removed)
173        return
174
175    # add lines (missing \n between functions/classes)
176    added = 0
177    for fname, entries in table.items():
178        added += add_lines(fname, entries)
179    if added:
180        print("%s lines were added from some file(s); please re-run" %
181              added)
182        return
183
184
185main()
186