summaryrefslogtreecommitdiff
path: root/extra/release/flakeplus.py
blob: 6fe1f1fc2abe4478e7d0ab22426dd21787f70b98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import with_statement

import os
import re
import sys

from collections import defaultdict
from unipath import Path

RE_COMMENT = r'^\s*\#'
RE_NOQA = r'.+?\#\s+noqa+'
RE_MULTILINE_COMMENT_O = r'^\s*(?:\'\'\'|""").+?(?:\'\'\'|""")'
RE_MULTILINE_COMMENT_S = r'^\s*(?:\'\'\'|""")'
RE_MULTILINE_COMMENT_E = r'(?:^|.+?)(?:\'\'\'|""")'
RE_WITH = r'(?:^|\s+)with\s+'
RE_WITH_IMPORT = r'''from\s+ __future__\s+ import\s+ with_statement'''
RE_PRINT = r'''(?:^|\s+)print\((?:"|')(?:\W+?)?[A-Z0-9:]{2,}'''
RE_ABS_IMPORT = r'''from\s+ __future__\s+ import\s+ absolute_import'''

acc = defaultdict(lambda: {"abs": False, "print": False})


def compile(regex):
    return re.compile(regex, re.VERBOSE)


class FlakePP(object):
    re_comment = compile(RE_COMMENT)
    re_ml_comment_o = compile(RE_MULTILINE_COMMENT_O)
    re_ml_comment_s = compile(RE_MULTILINE_COMMENT_S)
    re_ml_comment_e = compile(RE_MULTILINE_COMMENT_E)
    re_abs_import = compile(RE_ABS_IMPORT)
    re_print = compile(RE_PRINT)
    re_with_import = compile(RE_WITH_IMPORT)
    re_with = compile(RE_WITH)
    re_noqa = compile(RE_NOQA)
    map = {"abs": True, "print": False,
            "with": False, "with-used": False}

    def __init__(self, verbose=False):
        self.verbose = verbose
        self.steps = (("abs", self.re_abs_import),
                      ("with", self.re_with_import),
                      ("with-used", self.re_with),
                      ("print", self.re_print))

    def analyze_fh(self, fh):
        steps = self.steps
        filename = fh.name
        acc = dict(self.map)
        index = 0
        errors = [0]

        def error(fmt, **kwargs):
            errors[0] += 1
            self.announce(fmt, **dict(kwargs, filename=filename))

        for index, line in enumerate(self.strip_comments(fh)):
            for key, pattern in steps:
                if pattern.match(line):
                    acc[key] = True
        if index:
            if not acc["abs"]:
                error("%(filename)s: missing abs import")
            if acc["with-used"] and not acc["with"]:
                error("%(filename)s: missing with import")
            if acc["print"]:
                error("%(filename)s: left over print statement")

        return filename, errors[0], acc

    def analyze_file(self, filename):
        with open(filename) as fh:
            return self.analyze_fh(fh)

    def analyze_tree(self, dir):
        for dirpath, _, filenames in os.walk(dir):
            for path in (Path(dirpath, f) for f in filenames):
                if path.endswith(".py"):
                    yield self.analyze_file(path)

    def analyze(self, *paths):
        for path in map(Path, paths):
            if path.isdir():
                for res in self.analyze_tree(path):
                    yield res
            else:
                yield self.analyze_file(path)

    def strip_comments(self, fh):
        re_comment = self.re_comment
        re_ml_comment_o = self.re_ml_comment_o
        re_ml_comment_s = self.re_ml_comment_s
        re_ml_comment_e = self.re_ml_comment_e
        re_noqa = self.re_noqa
        in_ml = False

        for line in fh.readlines():
            if in_ml:
                if re_ml_comment_e.match(line):
                    in_ml = False
            else:
                if re_noqa.match(line) or re_ml_comment_o.match(line):
                    pass
                elif re_ml_comment_s.match(line):
                    in_ml = True
                elif re_comment.match(line):
                    pass
                else:
                    yield line

    def announce(self, fmt, **kwargs):
        sys.stderr.write((fmt + "\n") % kwargs)


def main(argv=sys.argv, exitcode=0):
    for _, errors, _ in FlakePP(verbose=True).analyze(*argv[1:]):
        if errors:
            exitcode = 1
    return exitcode


if __name__ == "__main__":
    sys.exit(main())