1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0+ 3# 4# Copyright 2021 Google LLC 5# 6 7"""Changes the functions and class methods in a file to use snake case, updating 8other tools which use them""" 9 10from argparse import ArgumentParser 11import glob 12import os 13import re 14import subprocess 15 16import camel_case 17 18# Exclude functions with these names 19EXCLUDE_NAMES = set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass']) 20 21# Find function definitions in a file 22RE_FUNC = re.compile(r' *def (\w+)\(') 23 24# Where to find files that might call the file being converted 25FILES_GLOB = 'tools/**/*.py' 26 27def collect_funcs(fname): 28 """Collect a list of functions in a file 29 30 Args: 31 fname (str): Filename to read 32 33 Returns: 34 tuple: 35 str: contents of file 36 list of str: List of function names 37 """ 38 with open(fname, encoding='utf-8') as inf: 39 data = inf.read() 40 funcs = RE_FUNC.findall(data) 41 return data, funcs 42 43def get_module_name(fname): 44 """Convert a filename to a module name 45 46 Args: 47 fname (str): Filename to convert, e.g. 'tools/patman/command.py' 48 49 Returns: 50 tuple: 51 str: Full module name, e.g. 'patman.command' 52 str: Leaf module name, e.g. 'command' 53 str: Program name, e.g. 'patman' 54 """ 55 parts = os.path.splitext(fname)[0].split('/')[1:] 56 module_name = '.'.join(parts) 57 return module_name, parts[-1], parts[0] 58 59def process_caller(data, conv, module_name, leaf): 60 """Process a file that might call another module 61 62 This converts all the camel-case references in the provided file contents 63 with the corresponding snake-case references. 64 65 Args: 66 data (str): Contents of file to convert 67 conv (dict): Identifies to convert 68 key: Current name in camel case, e.g. 'DoIt' 69 value: New name in snake case, e.g. 'do_it' 70 module_name: Name of module as referenced by the file, e.g. 71 'patman.command' 72 leaf: Leaf module name, e.g. 'command' 73 74 Returns: 75 str: New file contents, or None if it was not modified 76 """ 77 total = 0 78 79 # Update any simple functions calls into the module 80 for name, new_name in conv.items(): 81 newdata, count = re.subn(fr'{leaf}.{name}\(', 82 f'{leaf}.{new_name}(', data) 83 total += count 84 data = newdata 85 86 # Deal with files that import symbols individually 87 imports = re.findall(fr'from {module_name} import (.*)\n', data) 88 for item in imports: 89 #print('item', item) 90 names = [n.strip() for n in item.split(',')] 91 new_names = [conv.get(n) or n for n in names] 92 new_line = f"from {module_name} import {', '.join(new_names)}\n" 93 data = re.sub(fr'from {module_name} import (.*)\n', new_line, data) 94 for name in names: 95 new_name = conv.get(name) 96 if new_name: 97 newdata = re.sub(fr'\b{name}\(', f'{new_name}(', data) 98 data = newdata 99 100 # Deal with mocks like: 101 # unittest.mock.patch.object(module, 'Function', ... 102 for name, new_name in conv.items(): 103 newdata, count = re.subn(fr"{leaf}, '{name}'", 104 f"{leaf}, '{new_name}'", data) 105 total += count 106 data = newdata 107 108 if total or imports: 109 return data 110 return None 111 112def process_file(srcfile, do_write, commit): 113 """Process a file to rename its camel-case functions 114 115 This renames the class methods and functions in a file so that they use 116 snake case. Then it updates other modules that call those functions. 117 118 Args: 119 srcfile (str): Filename to process 120 do_write (bool): True to write back to files, False to do a dry run 121 commit (bool): True to create a commit with the changes 122 """ 123 data, funcs = collect_funcs(srcfile) 124 module_name, leaf, prog = get_module_name(srcfile) 125 #print('module_name', module_name) 126 #print(len(funcs)) 127 #print(funcs[0]) 128 conv = {} 129 for name in funcs: 130 if name not in EXCLUDE_NAMES: 131 conv[name] = camel_case.to_snake(name) 132 133 # Convert name to new_name in the file 134 for name, new_name in conv.items(): 135 #print(name, new_name) 136 # Don't match if it is preceded by a '.', since that indicates that 137 # it is calling this same function name but in a different module 138 newdata = re.sub(fr'(?<!\.){name}\(', f'{new_name}(', data) 139 data = newdata 140 141 # But do allow self.xxx 142 newdata = re.sub(fr'self.{name}\(', f'self.{new_name}(', data) 143 data = newdata 144 if do_write: 145 with open(srcfile, 'w', encoding='utf-8') as out: 146 out.write(data) 147 148 # Now find all files which use these functions and update them 149 for fname in glob.glob(FILES_GLOB, recursive=True): 150 with open(fname, encoding='utf-8') as inf: 151 data = inf.read() 152 newdata = process_caller(fname, conv, module_name, leaf) 153 if do_write and newdata: 154 with open(fname, 'w', encoding='utf-8') as out: 155 out.write(newdata) 156 157 if commit: 158 subprocess.call(['git', 'add', '-u']) 159 subprocess.call([ 160 'git', 'commit', '-s', '-m', 161 f'''{prog}: Convert camel case in {os.path.basename(srcfile)} 162 163Convert this file to snake case and update all files which use it. 164''']) 165 166 167def main(): 168 """Main program""" 169 epilog = 'Convert camel case function names to snake in a file and callers' 170 parser = ArgumentParser(epilog=epilog) 171 parser.add_argument('-c', '--commit', action='store_true', 172 help='Add a commit with the changes') 173 parser.add_argument('-n', '--dry_run', action='store_true', 174 help='Dry run, do not write back to files') 175 parser.add_argument('-s', '--srcfile', type=str, required=True, help='Filename to convert') 176 args = parser.parse_args() 177 process_file(args.srcfile, not args.dry_run, args.commit) 178 179if __name__ == '__main__': 180 main() 181