123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- #!/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()
|