style.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: GPL-2.0+
  3. #
  4. # Copyright 2021 Google LLC
  5. #
  6. """Changes the functions and class methods in a file to use snake case, updating
  7. other tools which use them"""
  8. from argparse import ArgumentParser
  9. import glob
  10. import os
  11. import re
  12. import subprocess
  13. import camel_case
  14. # Exclude functions with these names
  15. EXCLUDE_NAMES = set(['setUp', 'tearDown', 'setUpClass', 'tearDownClass'])
  16. # Find function definitions in a file
  17. RE_FUNC = re.compile(r' *def (\w+)\(')
  18. # Where to find files that might call the file being converted
  19. FILES_GLOB = 'tools/**/*.py'
  20. def collect_funcs(fname):
  21. """Collect a list of functions in a file
  22. Args:
  23. fname (str): Filename to read
  24. Returns:
  25. tuple:
  26. str: contents of file
  27. list of str: List of function names
  28. """
  29. with open(fname, encoding='utf-8') as inf:
  30. data = inf.read()
  31. funcs = RE_FUNC.findall(data)
  32. return data, funcs
  33. def get_module_name(fname):
  34. """Convert a filename to a module name
  35. Args:
  36. fname (str): Filename to convert, e.g. 'tools/patman/command.py'
  37. Returns:
  38. tuple:
  39. str: Full module name, e.g. 'patman.command'
  40. str: Leaf module name, e.g. 'command'
  41. str: Program name, e.g. 'patman'
  42. """
  43. parts = os.path.splitext(fname)[0].split('/')[1:]
  44. module_name = '.'.join(parts)
  45. return module_name, parts[-1], parts[0]
  46. def process_caller(data, conv, module_name, leaf):
  47. """Process a file that might call another module
  48. This converts all the camel-case references in the provided file contents
  49. with the corresponding snake-case references.
  50. Args:
  51. data (str): Contents of file to convert
  52. conv (dict): Identifies to convert
  53. key: Current name in camel case, e.g. 'DoIt'
  54. value: New name in snake case, e.g. 'do_it'
  55. module_name: Name of module as referenced by the file, e.g.
  56. 'patman.command'
  57. leaf: Leaf module name, e.g. 'command'
  58. Returns:
  59. str: New file contents, or None if it was not modified
  60. """
  61. total = 0
  62. # Update any simple functions calls into the module
  63. for name, new_name in conv.items():
  64. newdata, count = re.subn(fr'{leaf}.{name}\(',
  65. f'{leaf}.{new_name}(', data)
  66. total += count
  67. data = newdata
  68. # Deal with files that import symbols individually
  69. imports = re.findall(fr'from {module_name} import (.*)\n', data)
  70. for item in imports:
  71. #print('item', item)
  72. names = [n.strip() for n in item.split(',')]
  73. new_names = [conv.get(n) or n for n in names]
  74. new_line = f"from {module_name} import {', '.join(new_names)}\n"
  75. data = re.sub(fr'from {module_name} import (.*)\n', new_line, data)
  76. for name in names:
  77. new_name = conv.get(name)
  78. if new_name:
  79. newdata = re.sub(fr'\b{name}\(', f'{new_name}(', data)
  80. data = newdata
  81. # Deal with mocks like:
  82. # unittest.mock.patch.object(module, 'Function', ...
  83. for name, new_name in conv.items():
  84. newdata, count = re.subn(fr"{leaf}, '{name}'",
  85. f"{leaf}, '{new_name}'", data)
  86. total += count
  87. data = newdata
  88. if total or imports:
  89. return data
  90. return None
  91. def process_file(srcfile, do_write, commit):
  92. """Process a file to rename its camel-case functions
  93. This renames the class methods and functions in a file so that they use
  94. snake case. Then it updates other modules that call those functions.
  95. Args:
  96. srcfile (str): Filename to process
  97. do_write (bool): True to write back to files, False to do a dry run
  98. commit (bool): True to create a commit with the changes
  99. """
  100. data, funcs = collect_funcs(srcfile)
  101. module_name, leaf, prog = get_module_name(srcfile)
  102. #print('module_name', module_name)
  103. #print(len(funcs))
  104. #print(funcs[0])
  105. conv = {}
  106. for name in funcs:
  107. if name not in EXCLUDE_NAMES:
  108. conv[name] = camel_case.to_snake(name)
  109. # Convert name to new_name in the file
  110. for name, new_name in conv.items():
  111. #print(name, new_name)
  112. # Don't match if it is preceded by a '.', since that indicates that
  113. # it is calling this same function name but in a different module
  114. newdata = re.sub(fr'(?<!\.){name}\(', f'{new_name}(', data)
  115. data = newdata
  116. # But do allow self.xxx
  117. newdata = re.sub(fr'self.{name}\(', f'self.{new_name}(', data)
  118. data = newdata
  119. if do_write:
  120. with open(srcfile, 'w', encoding='utf-8') as out:
  121. out.write(data)
  122. # Now find all files which use these functions and update them
  123. for fname in glob.glob(FILES_GLOB, recursive=True):
  124. with open(fname, encoding='utf-8') as inf:
  125. data = inf.read()
  126. newdata = process_caller(fname, conv, module_name, leaf)
  127. if do_write and newdata:
  128. with open(fname, 'w', encoding='utf-8') as out:
  129. out.write(newdata)
  130. if commit:
  131. subprocess.call(['git', 'add', '-u'])
  132. subprocess.call([
  133. 'git', 'commit', '-s', '-m',
  134. f'''{prog}: Convert camel case in {os.path.basename(srcfile)}
  135. Convert this file to snake case and update all files which use it.
  136. '''])
  137. def main():
  138. """Main program"""
  139. epilog = 'Convert camel case function names to snake in a file and callers'
  140. parser = ArgumentParser(epilog=epilog)
  141. parser.add_argument('-c', '--commit', action='store_true',
  142. help='Add a commit with the changes')
  143. parser.add_argument('-n', '--dry_run', action='store_true',
  144. help='Dry run, do not write back to files')
  145. parser.add_argument('-s', '--srcfile', type=str, required=True, help='Filename to convert')
  146. args = parser.parse_args()
  147. process_file(args.srcfile, not args.dry_run, args.commit)
  148. if __name__ == '__main__':
  149. main()