#! /usr/bin/env python # Copyright 2015 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import cStringIO import functools import imp import inspect import itertools import operator import os import re import sys from catapult_base import refactor from catapult_base.refactor_util import move from telemetry.internal.util import command_line from telemetry.internal.util import path _RELATIVE_BASE_DIRS = ( ('chrome', 'test', 'telemetry'), ('content', 'test', 'gpu'), ('tools', 'bisect-manual-test.py'), ('tools', 'chrome_proxy'), ('tools', 'perf'), ('tools', 'profile_chrome', 'perf_controller.py'), ('tools', 'run-bisect-manual-test.py'), ('third_party', 'skia', 'tools', 'skp', 'page_sets'), ('third_party', 'trace-viewer'), ) # All folders dependent on Telemetry, found using a code search. # Note that this is not the same as the directory that imports are relative to. BASE_DIRS = [path.GetTelemetryDir()] + [ os.path.join(path.GetChromiumSrcDir(), *dir_path) for dir_path in _RELATIVE_BASE_DIRS] def SortImportGroups(module_path): """Sort each group of imports in the given Python module. A group is a collection of adjacent import statements, with no non-import lines in between. Groups are sorted according to the Google Python Style Guide: "lexicographically, ignoring case, according to each module's full package path." """ _TransformImportGroups(module_path, _SortImportGroup) def _SortImportGroup(import_group): def _ImportComparator(import1, import2): _, root1, module1, _, _ = import1 _, root2, module2, _, _ = import2 full_module1 = (root1 + '.' + module1 if root1 else module1).lower() full_module2 = (root2 + '.' + module2 if root2 else module2).lower() return cmp(full_module1, full_module2) return sorted(import_group, cmp=_ImportComparator) def _TransformImportGroups(module_path, transformation): """Apply a transformation to each group of imports in the given module. An import is a tuple of (indent, root, module, alias, suffix), serialized as from import as . Args: module_path: The module to apply transformations on. transformation: A function that takes in an import group and returns a modified import group. An import group is a list of import tuples. Returns: True iff the module was modified, and False otherwise. """ def _WriteImports(output_stream, import_group): for indent, root, module, alias, suffix in transformation(import_group): output_stream.write(indent) if root: output_stream.write('from ') output_stream.write(root) output_stream.write(' ') output_stream.write('import ') output_stream.write(module) if alias: output_stream.write(' as ') output_stream.write(alias) output_stream.write(suffix) output_stream.write('\n') # Read the file so we can diff it later to determine if we made any changes. with open(module_path, 'r') as module_file: original_file = module_file.read() # Locate imports using regex, group them, and transform each one. # This regex produces a tuple of (indent, root, module, alias, suffix). regex = (r'(\s*)(?:from ((?:[a-z0-9_]+\.)*[a-z0-9_]+) )?' r'import ((?:[a-z0-9_]+\.)*[A-Za-z0-9_]+)(?: as ([A-Za-z0-9_]+))?(.*)') pattern = re.compile(regex) updated_file = cStringIO.StringIO() with open(module_path, 'r') as module_file: import_group = [] for line in module_file: import_match = pattern.match(line) if import_match: import_group.append(list(import_match.groups())) continue if not import_group: updated_file.write(line) continue _WriteImports(updated_file, import_group) import_group = [] updated_file.write(line) if import_group: _WriteImports(updated_file, import_group) import_group = [] if original_file == updated_file.getvalue(): return False with open(module_path, 'w') as module_file: module_file.write(updated_file.getvalue()) return True def _CountInterfaces(module): return (len(list(module.FindChildren(refactor.Class))) + len(list(module.FindChildren(refactor.Function)))) def _IsSourceDir(dir_name): return dir_name[0] != '.' and dir_name != 'third_party' def _IsPythonModule(file_name): _, ext = os.path.splitext(file_name) return ext == '.py' class Count(command_line.Command): """Print the number of public modules.""" @classmethod def AddCommandLineArgs(cls, parser): parser.add_argument('type', nargs='?', choices=('interfaces', 'modules'), default='modules') def Run(self, args): module_paths = path.ListFiles( path.GetTelemetryDir(), self._IsPublicApiDir, self._IsPublicApiFile) if args.type == 'modules': print len(module_paths) elif args.type == 'interfaces': print reduce(operator.add, refactor.Transform(_CountInterfaces, module_paths)) return 0 @staticmethod def _IsPublicApiDir(dir_name): return (dir_name[0] != '.' and dir_name[0] != '_' and dir_name != 'internal' and dir_name != 'third_party') @staticmethod def _IsPublicApiFile(file_name): root, ext = os.path.splitext(file_name) return (file_name[0] != '.' and not root.endswith('_unittest') and ext == '.py') def _TelemetryFiles(): list_files = functools.partial(path.ListFiles, should_include_dir=_IsSourceDir, should_include_file=_IsPythonModule) return sorted(itertools.chain(*map(list_files, BASE_DIRS))) class Mv(command_line.Command): """Move modules or packages.""" @classmethod def AddCommandLineArgs(cls, parser): parser.add_argument('source', nargs='+') parser.add_argument('target') @classmethod def ProcessCommandLineArgs(cls, parser, args): # Check source file paths. for source_path in args.source: # Ensure source path exists. if not os.path.exists(source_path): parser.error('"%s" not found.' % source_path) # Ensure source path is in one of the BASE_DIRS. for base_dir in BASE_DIRS: if path.IsSubpath(source_path, base_dir): break else: parser.error('"%s" is not in any of the base dirs.') # Ensure target directory exists. if not (os.path.exists(args.target) or os.path.exists(os.path.dirname(args.target))): parser.error('"%s" not found.' % args.target) # Ensure target path is in one of the BASE_DIRS. for base_dir in BASE_DIRS: if path.IsSubpath(args.target, base_dir): break else: parser.error('"%s" is not in any of the base dirs.') # If there are multiple source paths, ensure target is a directory. if len(args.source) > 1 and not os.path.isdir(args.target): parser.error('Target "%s" is not a directory.' % args.target) # Ensure target is not in any of the source paths. for source_path in args.source: if path.IsSubpath(args.target, source_path): parser.error('Cannot move "%s" to a subdirectory of itself, "%s".' % (source_path, args.target)) def Run(self, args): move.Run(args.source, args.target, _TelemetryFiles()) for module_path in _TelemetryFiles(): SortImportGroups(module_path) return 0 class Sort(command_line.Command): """Sort imports.""" @classmethod def AddCommandLineArgs(cls, parser): parser.add_argument('target', nargs='*') @classmethod def ProcessCommandLineArgs(cls, parser, args): for target in args.target: if not os.path.exists(target): parser.error('"%s" not found.' % target) def Run(self, args): if args.target: targets = args.target else: targets = BASE_DIRS for base_dir in targets: for module_path in path.ListFiles(base_dir, _IsSourceDir, _IsPythonModule): SortImportGroups(module_path) return 0 class RefactorCommand(command_line.SubcommandCommand): commands = (Count, Mv, Sort,) if __name__ == '__main__': sys.exit(RefactorCommand.main())