summaryrefslogtreecommitdiff
path: root/scripts/style.py
blob: 7b73b007dea8a0fd054c190a4188f66c65882fec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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()