#!/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'(?