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