diff options
Diffstat (limited to 'scripts/style.py')
-rwxr-xr-x | scripts/style.py | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/scripts/style.py b/scripts/style.py new file mode 100755 index 0000000..7b73b00 --- /dev/null +++ b/scripts/style.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2021 Google LLC +# + +"""Changes the functions and class methods in a file to use snake case, updating +other tools which use them""" + +from argparse import ArgumentParser +import glob +import os +import re +import subprocess + +import camel_case + +# Exclude functions with these names +EXCLUDE_NAMES = set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass']) + +# Find function definitions in a file +RE_FUNC = re.compile(r' *def (\w+)\(') + +# Where to find files that might call the file being converted +FILES_GLOB = 'tools/**/*.py' + +def collect_funcs(fname): + """Collect a list of functions in a file + + Args: + fname (str): Filename to read + + Returns: + tuple: + str: contents of file + list of str: List of function names + """ + with open(fname, encoding='utf-8') as inf: + data = inf.read() + funcs = RE_FUNC.findall(data) + return data, funcs + +def get_module_name(fname): + """Convert a filename to a module name + + Args: + fname (str): Filename to convert, e.g. 'tools/patman/command.py' + + Returns: + tuple: + str: Full module name, e.g. 'patman.command' + str: Leaf module name, e.g. 'command' + str: Program name, e.g. 'patman' + """ + parts = os.path.splitext(fname)[0].split('/')[1:] + module_name = '.'.join(parts) + return module_name, parts[-1], parts[0] + +def process_caller(data, conv, module_name, leaf): + """Process a file that might call another module + + This converts all the camel-case references in the provided file contents + with the corresponding snake-case references. + + Args: + data (str): Contents of file to convert + conv (dict): Identifies to convert + key: Current name in camel case, e.g. 'DoIt' + value: New name in snake case, e.g. 'do_it' + module_name: Name of module as referenced by the file, e.g. + 'patman.command' + leaf: Leaf module name, e.g. 'command' + + Returns: + str: New file contents, or None if it was not modified + """ + total = 0 + + # Update any simple functions calls into the module + for name, new_name in conv.items(): + newdata, count = re.subn(fr'{leaf}.{name}\(', + f'{leaf}.{new_name}(', data) + total += count + data = newdata + + # Deal with files that import symbols individually + imports = re.findall(fr'from {module_name} import (.*)\n', data) + for item in imports: + #print('item', item) + names = [n.strip() for n in item.split(',')] + new_names = [conv.get(n) or n for n in names] + new_line = f"from {module_name} import {', '.join(new_names)}\n" + data = re.sub(fr'from {module_name} import (.*)\n', new_line, data) + for name in names: + new_name = conv.get(name) + if new_name: + newdata = re.sub(fr'\b{name}\(', f'{new_name}(', data) + data = newdata + + # Deal with mocks like: + # unittest.mock.patch.object(module, 'Function', ... + for name, new_name in conv.items(): + newdata, count = re.subn(fr"{leaf}, '{name}'", + f"{leaf}, '{new_name}'", data) + total += count + data = newdata + + if total or imports: + return data + return None + +def process_file(srcfile, do_write, commit): + """Process a file to rename its camel-case functions + + This renames the class methods and functions in a file so that they use + snake case. Then it updates other modules that call those functions. + + Args: + srcfile (str): Filename to process + do_write (bool): True to write back to files, False to do a dry run + commit (bool): True to create a commit with the changes + """ + data, funcs = collect_funcs(srcfile) + module_name, leaf, prog = get_module_name(srcfile) + #print('module_name', module_name) + #print(len(funcs)) + #print(funcs[0]) + conv = {} + for name in funcs: + if name not in EXCLUDE_NAMES: + conv[name] = camel_case.to_snake(name) + + # Convert name to new_name in the file + for name, new_name in conv.items(): + #print(name, new_name) + # Don't match if it is preceded by a '.', since that indicates that + # it is calling this same function name but in a different module + newdata = re.sub(fr'(?<!\.){name}\(', f'{new_name}(', data) + data = newdata + + # But do allow self.xxx + newdata = re.sub(fr'self.{name}\(', f'self.{new_name}(', data) + data = newdata + if do_write: + with open(srcfile, 'w', encoding='utf-8') as out: + out.write(data) + + # Now find all files which use these functions and update them + for fname in glob.glob(FILES_GLOB, recursive=True): + with open(fname, encoding='utf-8') as inf: + data = inf.read() + newdata = process_caller(fname, conv, module_name, leaf) + if do_write and newdata: + with open(fname, 'w', encoding='utf-8') as out: + out.write(newdata) + + if commit: + subprocess.call(['git', 'add', '-u']) + subprocess.call([ + 'git', 'commit', '-s', '-m', + f'''{prog}: Convert camel case in {os.path.basename(srcfile)} + +Convert this file to snake case and update all files which use it. +''']) + + +def main(): + """Main program""" + epilog = 'Convert camel case function names to snake in a file and callers' + parser = ArgumentParser(epilog=epilog) + parser.add_argument('-c', '--commit', action='store_true', + help='Add a commit with the changes') + parser.add_argument('-n', '--dry_run', action='store_true', + help='Dry run, do not write back to files') + parser.add_argument('-s', '--srcfile', type=str, required=True, help='Filename to convert') + args = parser.parse_args() + process_file(args.srcfile, not args.dry_run, args.commit) + +if __name__ == '__main__': + main() |