Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/env python3
      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     with subprocess.Popen(cmd.split(),
     43                           stdout=subprocess.PIPE,
     44                           stderr=subprocess.PIPE) as st:
     45         bstdout, _ = st.communicate()
     46         return st.returncode == 0 and bstdout
     47 
     48 
     49 @status("Getting the list of files that have been added/changed",
     50         info=lambda x: n_files_str(len(x)))
     51 def changed_files():
     52     """Get the list of changed or added files from Mercurial or git."""
     53     if os.path.isdir(os.path.join(SRCDIR, '.hg')):
     54         cmd = 'hg status --added --modified --no-status'
     55         if mq_patches_applied():
     56             cmd += ' --rev qparent'
     57         with subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) as st:
     58             return [x.decode().rstrip() for x in st.stdout]
     59     elif os.path.isdir(os.path.join(SRCDIR, '.git')):
     60         cmd = 'git status --porcelain'
     61         filenames = []
     62         with subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) as st:
     63             for line in st.stdout:
     64                 line = line.decode().rstrip()
     65                 status = set(line[:2])
     66                 # modified, added or unmerged files
     67                 if not status.intersection('MAU'):
     68                     continue
     69                 filename = line[3:]
     70                 if ' -> ' in filename:
     71                     # file is renamed
     72                     filename = filename.split(' -> ', 2)[1].strip()
     73                 filenames.append(filename)
     74         return filenames
     75     else:
     76         sys.exit('need a Mercurial or git checkout to get modified files')
     77 
     78 
     79 def report_modified_files(file_paths):
     80     count = len(file_paths)
     81     if count == 0:
     82         return n_files_str(count)
     83     else:
     84         lines = ["{}:".format(n_files_str(count))]
     85         for path in file_paths:
     86             lines.append("  {}".format(path))
     87         return "\n".join(lines)
     88 
     89 
     90 @status("Fixing whitespace", info=report_modified_files)
     91 def normalize_whitespace(file_paths):
     92     """Make sure that the whitespace for .py files have been normalized."""
     93     reindent.makebackup = False  # No need to create backups.
     94     fixed = [path for path in file_paths if path.endswith('.py') and
     95              reindent.check(os.path.join(SRCDIR, path))]
     96     return fixed
     97 
     98 
     99 @status("Fixing C file whitespace", info=report_modified_files)
    100 def normalize_c_whitespace(file_paths):
    101     """Report if any C files """
    102     fixed = []
    103     for path in file_paths:
    104         abspath = os.path.join(SRCDIR, path)
    105         with open(abspath, 'r') as f:
    106             if '\t' not in f.read():
    107                 continue
    108         untabify.process(abspath, 8, verbose=False)
    109         fixed.append(path)
    110     return fixed
    111 
    112 
    113 ws_re = re.compile(br'\s+(\r?\n)$')
    114 
    115 @status("Fixing docs whitespace", info=report_modified_files)
    116 def normalize_docs_whitespace(file_paths):
    117     fixed = []
    118     for path in file_paths:
    119         abspath = os.path.join(SRCDIR, path)
    120         try:
    121             with open(abspath, 'rb') as f:
    122                 lines = f.readlines()
    123             new_lines = [ws_re.sub(br'\1', line) for line in lines]
    124             if new_lines != lines:
    125                 shutil.copyfile(abspath, abspath + '.bak')
    126                 with open(abspath, 'wb') as f:
    127                     f.writelines(new_lines)
    128                 fixed.append(path)
    129         except Exception as err:
    130             print('Cannot fix %s: %s' % (path, err))
    131     return fixed
    132 
    133 
    134 @status("Docs modified", modal=True)
    135 def docs_modified(file_paths):
    136     """Report if any file in the Doc directory has been changed."""
    137     return bool(file_paths)
    138 
    139 
    140 @status("Misc/ACKS updated", modal=True)
    141 def credit_given(file_paths):
    142     """Check if Misc/ACKS has been changed."""
    143     return os.path.join('Misc', 'ACKS') in file_paths
    144 
    145 
    146 @status("Misc/NEWS updated", modal=True)
    147 def reported_news(file_paths):
    148     """Check if Misc/NEWS has been changed."""
    149     return os.path.join('Misc', 'NEWS') in file_paths
    150 
    151 @status("configure regenerated", modal=True, info=str)
    152 def regenerated_configure(file_paths):
    153     """Check if configure has been regenerated."""
    154     if 'configure.ac' in file_paths:
    155         return "yes" if 'configure' in file_paths else "no"
    156     else:
    157         return "not needed"
    158 
    159 @status("pyconfig.h.in regenerated", modal=True, info=str)
    160 def regenerated_pyconfig_h_in(file_paths):
    161     """Check if pyconfig.h.in has been regenerated."""
    162     if 'configure.ac' in file_paths:
    163         return "yes" if 'pyconfig.h.in' in file_paths else "no"
    164     else:
    165         return "not needed"
    166 
    167 def main():
    168     file_paths = changed_files()
    169     python_files = [fn for fn in file_paths if fn.endswith('.py')]
    170     c_files = [fn for fn in file_paths if fn.endswith(('.c', '.h'))]
    171     doc_files = [fn for fn in file_paths if fn.startswith('Doc') and
    172                  fn.endswith(('.rst', '.inc'))]
    173     misc_files = {os.path.join('Misc', 'ACKS'), os.path.join('Misc', 'NEWS')}\
    174             & set(file_paths)
    175     # PEP 8 whitespace rules enforcement.
    176     normalize_whitespace(python_files)
    177     # C rules enforcement.
    178     normalize_c_whitespace(c_files)
    179     # Doc whitespace enforcement.
    180     normalize_docs_whitespace(doc_files)
    181     # Docs updated.
    182     docs_modified(doc_files)
    183     # Misc/ACKS changed.
    184     credit_given(misc_files)
    185     # Misc/NEWS changed.
    186     reported_news(misc_files)
    187     # Regenerated configure, if necessary.
    188     regenerated_configure(file_paths)
    189     # Regenerated pyconfig.h.in, if necessary.
    190     regenerated_pyconfig_h_in(file_paths)
    191 
    192     # Test suite run and passed.
    193     if python_files or c_files:
    194         end = " and check for refleaks?" if c_files else "?"
    195         print()
    196         print("Did you run the test suite" + end)
    197 
    198 
    199 if __name__ == '__main__':
    200     main()
    201