2 # SPDX-License-Identifier: GPL-2.0+
4 # Copyright 2021 Google LLC
7 """Changes the functions and class methods in a file to use snake case, updating
8 other tools which use them"""
10 from argparse
import ArgumentParser
18 # Exclude functions with these names
19 EXCLUDE_NAMES
= set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass'])
21 # Find function definitions in a file
22 RE_FUNC
= re
.compile(r
' *def (\w+)\(')
24 # Where to find files that might call the file being converted
25 FILES_GLOB
= 'tools/**/*.py'
27 def collect_funcs(fname
):
28 """Collect a list of functions in a file
31 fname (str): Filename to read
36 list of str: List of function names
38 with
open(fname
, encoding
='utf-8') as inf
:
40 funcs
= RE_FUNC
.findall(data
)
43 def get_module_name(fname
):
44 """Convert a filename to a module name
47 fname (str): Filename to convert, e.g. 'tools/patman/command.py'
51 str: Full module name, e.g. 'patman.command'
52 str: Leaf module name, e.g. 'command'
53 str: Program name, e.g. 'patman'
55 parts
= os
.path
.splitext(fname
)[0].split('/')[1:]
56 module_name
= '.'.join(parts
)
57 return module_name
, parts
[-1], parts
[0]
59 def process_caller(data
, conv
, module_name
, leaf
):
60 """Process a file that might call another module
62 This converts all the camel-case references in the provided file contents
63 with the corresponding snake-case references.
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.
72 leaf: Leaf module name, e.g. 'command'
75 str: New file contents, or None if it was not modified
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
)
86 # Deal with files that import symbols individually
87 imports
= re
.findall(fr
'from {module_name} import (.*)\n', data
)
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
)
95 new_name
= conv
.get(name
)
97 newdata
= re
.sub(fr
'\b{name}\(', f
'{new_name}(', data
)
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
)
112 def process_file(srcfile
, do_write
, commit
):
113 """Process a file to rename its camel-case functions
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.
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
123 data
, funcs
= collect_funcs(srcfile
)
124 module_name
, leaf
, prog
= get_module_name(srcfile
)
125 #print('module_name', module_name)
130 if name
not in EXCLUDE_NAMES
:
131 conv
[name
] = camel_case
.to_snake(name
)
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
)
141 # But do allow self.xxx
142 newdata
= re
.sub(fr
'self.{name}\(', f
'self.{new_name}(', data
)
145 with
open(srcfile
, 'w', encoding
='utf-8') as out
:
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
:
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
:
158 subprocess
.call(['git', 'add', '-u'])
160 'git', 'commit', '-s', '-m',
161 f
'''{prog}: Convert camel case in {os.path.basename(srcfile)}
163 Convert this file to snake case and update all files which use it.
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
)
179 if __name__
== '__main__':