1import re
2import sys
3import shutil
4import os.path
5import subprocess
6
7import reindent
8import untabify
9
10
11def n_files_str(count):
12    """Return 'N file(s)' with the proper plurality on 'file'."""
13    return "{} file{}".format(count, "s" if count != 1 else "")
14
15
16def status(message, modal=False, info=None):
17    """Decorator to output status info to stdout."""
18    def decorated_fxn(fxn):
19        def call_fxn(*args, **kwargs):
20            sys.stdout.write(message + ' ... ')
21            sys.stdout.flush()
22            result = fxn(*args, **kwargs)
23            if not modal and not info:
24                print "done"
25            elif info:
26                print info(result)
27            else:
28                print "yes" if result else "NO"
29            return result
30        return call_fxn
31    return decorated_fxn
32
33
34@status("Getting the list of files that have been added/changed",
35        info=lambda x: n_files_str(len(x)))
36def changed_files():
37    """Get the list of changed or added files from the VCS."""
38    if os.path.isdir('.hg'):
39        vcs = 'hg'
40        cmd = 'hg status --added --modified --no-status'
41    elif os.path.isdir('.svn'):
42        vcs = 'svn'
43        cmd = 'svn status --quiet --non-interactive --ignore-externals'
44    else:
45        sys.exit('need a checkout to get modified files')
46
47    st = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
48    try:
49        st.wait()
50        if vcs == 'hg':
51            return [x.decode().rstrip() for x in st.stdout]
52        else:
53            output = (x.decode().rstrip().rsplit(None, 1)[-1]
54                      for x in st.stdout if x[0] in 'AM')
55        return set(path for path in output if os.path.isfile(path))
56    finally:
57        st.stdout.close()
58
59
60def report_modified_files(file_paths):
61    count = len(file_paths)
62    if count == 0:
63        return n_files_str(count)
64    else:
65        lines = ["{}:".format(n_files_str(count))]
66        for path in file_paths:
67            lines.append("  {}".format(path))
68        return "\n".join(lines)
69
70
71@status("Fixing whitespace", info=report_modified_files)
72def normalize_whitespace(file_paths):
73    """Make sure that the whitespace for .py files have been normalized."""
74    reindent.makebackup = False  # No need to create backups.
75    fixed = []
76    for path in (x for x in file_paths if x.endswith('.py')):
77        if reindent.check(path):
78            fixed.append(path)
79    return fixed
80
81
82@status("Fixing C file whitespace", info=report_modified_files)
83def normalize_c_whitespace(file_paths):
84    """Report if any C files """
85    fixed = []
86    for path in file_paths:
87        with open(path, 'r') as f:
88            if '\t' not in f.read():
89                continue
90        untabify.process(path, 8, verbose=False)
91        fixed.append(path)
92    return fixed
93
94
95ws_re = re.compile(br'\s+(\r?\n)$')
96
97@status("Fixing docs whitespace", info=report_modified_files)
98def normalize_docs_whitespace(file_paths):
99    fixed = []
100    for path in file_paths:
101        try:
102            with open(path, 'rb') as f:
103                lines = f.readlines()
104            new_lines = [ws_re.sub(br'\1', line) for line in lines]
105            if new_lines != lines:
106                shutil.copyfile(path, path + '.bak')
107                with open(path, 'wb') as f:
108                    f.writelines(new_lines)
109                fixed.append(path)
110        except Exception as err:
111            print 'Cannot fix %s: %s' % (path, err)
112    return fixed
113
114
115@status("Docs modified", modal=True)
116def docs_modified(file_paths):
117    """Report if any file in the Doc directory has been changed."""
118    return bool(file_paths)
119
120
121@status("Misc/ACKS updated", modal=True)
122def credit_given(file_paths):
123    """Check if Misc/ACKS has been changed."""
124    return 'Misc/ACKS' in file_paths
125
126
127@status("Misc/NEWS updated", modal=True)
128def reported_news(file_paths):
129    """Check if Misc/NEWS has been changed."""
130    return 'Misc/NEWS' in file_paths
131
132
133def main():
134    file_paths = changed_files()
135    python_files = [fn for fn in file_paths if fn.endswith('.py')]
136    c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))]
137    doc_files = [fn for fn in file_paths if fn.startswith('Doc')]
138    special_files = {'Misc/ACKS', 'Misc/NEWS'} & set(file_paths)
139    # PEP 8 whitespace rules enforcement.
140    normalize_whitespace(python_files)
141    # C rules enforcement.
142    normalize_c_whitespace(c_files)
143    # Doc whitespace enforcement.
144    normalize_docs_whitespace(doc_files)
145    # Docs updated.
146    docs_modified(doc_files)
147    # Misc/ACKS changed.
148    credit_given(special_files)
149    # Misc/NEWS changed.
150    reported_news(special_files)
151
152    # Test suite run and passed.
153    print
154    print "Did you run the test suite?"
155
156
157if __name__ == '__main__':
158    main()
159