Simon Glass | a89943b | 2022-01-29 14:14:20 -0700 | [diff] [blame] | 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 |
| 8 | other tools which use them""" |
| 9 | |
| 10 | from argparse import ArgumentParser |
| 11 | import glob |
| 12 | import os |
| 13 | import re |
| 14 | import subprocess |
| 15 | |
| 16 | import camel_case |
| 17 | |
| 18 | # Exclude functions with these names |
| 19 | EXCLUDE_NAMES = set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass']) |
| 20 | |
| 21 | # Find function definitions in a file |
| 22 | RE_FUNC = re.compile(r' *def (\w+)\(') |
| 23 | |
| 24 | # Where to find files that might call the file being converted |
| 25 | FILES_GLOB = 'tools/**/*.py' |
| 26 | |
| 27 | def 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 | |
| 43 | def 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 | |
| 59 | def 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 | |
| 112 | def 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 | |
| 163 | Convert this file to snake case and update all files which use it. |
| 164 | ''']) |
| 165 | |
| 166 | |
| 167 | def 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 | |
| 179 | if __name__ == '__main__': |
| 180 | main() |