def traverse(self) -> None: if not self.skip_file(): try: if C.PY38_PLUS: tree = ast.parse(self.source, type_comments=True) else: tree = ast.parse(self.source) except SyntaxError as e: print( color.paint(str(e), color.RED) + " at " + color.paint(self.path.as_posix(), color.GREEN)) return None else: relate(tree) name_scanner = _NameScanner() name_scanner.traverse(tree) self.names.extend(name_scanner.names) import_scanner = _ImportScanner( source=self.source, names=self.names, include_star_import=self.include_star_import, ) import_scanner.traverse(tree) self.imports.extend(import_scanner.imports) self.import_names.extend([imp.name for imp in self.imports])
def traverse(self) -> None: if self.skip_file(): return None try: if C.PY38_PLUS: tree = ast.parse(self.source, type_comments=True) else: tree = ast.parse(self.source) except SyntaxError as e: print( color.paint(str(e), color.RED) + " at " + color.paint(self.path.as_posix(), color.GREEN)) return None """ Set parent """ relate(tree) Scope.add_global_scope(tree) """ Name analyzer """ _NameAnalyzer().visit(tree) """ Receive items on the __all__ list """ importable_visitor = _ImportableAnalyzer() importable_visitor.visit(tree) for node in importable_visitor.importable_nodes: if isinstance(node, ast.Constant): Name.register( lineno=node.lineno, name=str(node.value), node=node, is_all=True, ) elif isinstance(node, ast.Str): Name.register(lineno=node.lineno, name=node.s, node=node, is_all=True) importable_visitor.clear() """ Import analyzer """ _ImportAnalyzer( source=self.source, include_star_import=self.include_star_import, ).traverse(tree) Scope.remove_current_scope()
def show(unused_import: List[C.ImportT], py_path: Path) -> None: for imp in unused_import: if isinstance(imp, ImportFrom) and imp.star and imp.suggestions: context = ( color.paint(f"from {imp.name} import *", color.RED) + " -> " + color.paint( f"from {imp.name} import {', '.join(imp.suggestions)}", color.GREEN, )) else: context = color.paint(imp.name, color.YELLOW) print(context + " at " + color.paint(py_path.as_posix(), color.GREEN) + ":" + color.paint(str(imp.lineno), color.GREEN))
def test_red_paint_on_win(self): action_content = paint(self.content, RED) if TERMINAL_SUPPORT_COLOR: expected_content = RED + self.content + RESET else: expected_content = self.content self.assertEqual(expected_content, action_content)
def traverse( self, source: Union[str, bytes], mode: str = "exec", parent: Optional[ast.AST] = None, ) -> None: try: if PY38_PLUS: tree = ast.parse(source, mode=mode, type_comments=True) else: tree = ast.parse(source, mode=mode) except SyntaxError as err: if self.show_error: print(color.paint(str(err), color.RED)) # pragma: no cover raise err relate(tree, parent=parent) self.visit(tree) """ Receive items on the __all__ list """ importable_visitor = ImportableVisitor() importable_visitor.traverse(self.source) for node in importable_visitor.importable_nodes: if isinstance(node, ast.Constant): self.names.append( Name(lineno=node.lineno, name=str(node.value))) elif isinstance(node, ast.Str): self.names.append(Name(lineno=node.lineno, name=node.s))
def check( path: Path, unused_imports: List[Union[Import, ImportFrom]], use_color_setting: bool, ) -> None: for imp in unused_imports: if isinstance(imp, ImportFrom) and imp.star and imp.suggestions: context = ( paint(f"from {imp.name} import *", RED, use_color_setting) + " -> " + paint( f"from {imp.name} import {', '.join(imp.suggestions)}", GREEN, use_color_setting, )) else: context = paint(imp.name, YELLOW, use_color_setting) print(context + " at " + paint(path.as_posix(), GREEN, use_color_setting) + ":" + paint(str(imp.lineno), GREEN, use_color_setting))
def read(self, path: Path) -> Tuple[str, str]: try: with tokenize.open(path) as stream: source = stream.read() encoding = stream.encoding except (OSError, SyntaxError) as err: if self.show_error: print(color.paint(str(err), color.RED)) # pragma: no cover return "", "utf-8" return source, encoding
def test_red_paint(self): action_content = paint(self.content, RED) expected_content = RED + self.content + RESET self.assertEqual(expected_content, action_content)
def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( prog="unimport", description=C.DESCRIPTION, epilog="Get rid of all unused imports 🥳", ) exclusive_group = parser.add_mutually_exclusive_group(required=False) parser.add_argument( "sources", default=[Path(".")], nargs="*", help="files and folders to find the unused imports.", action="store", type=Path, ) parser.add_argument( "-c", "--config", default=".", help="read configuration from PATH.", metavar="PATH", action="store", type=Path, ) parser.add_argument( "--include", help="file include pattern.", metavar="include", action="store", default=[], type=lambda value: [value], ) parser.add_argument( "--exclude", help="file exclude pattern.", metavar="exclude", action="store", default=[], type=lambda value: [value], ) parser.add_argument( "--gitignore", action="store_true", help="exclude .gitignore patterns. if present.", ) parser.add_argument( "--include-star-import", action="store_true", help="Include star imports during scanning and refactor.", ) parser.add_argument( "--show-error", action="store_true", help="Show or don't show errors captured during static analysis.", ) parser.add_argument( "-d", "--diff", action="store_true", help="Prints a diff of all the changes unimport would make to a file.", ) exclusive_group.add_argument( "-r", "--remove", action="store_true", help="remove unused imports automatically.", ) exclusive_group.add_argument( "-p", "--permission", action="store_true", help="Refactor permission after see diff.", ) parser.add_argument( "--requirements", action="store_true", help= "Include requirements.txt file, You can use it with all other arguments", ) parser.add_argument( "--check", action="store_true", help="Prints which file the unused imports are in.", ) parser.add_argument( "-v", "--version", action="version", version=f"Unimport {C.VERSION}", help="Prints version of unimport", ) argv = argv if argv is not None else sys.argv[1:] args = parser.parse_args(argv) session = Session( config_file=args.config, include_star_import=args.include_star_import, show_error=args.show_error, ) args.remove = args.remove or session.config.remove # type: ignore args.diff = any( (args.diff, args.permission, session.config.diff)) # type: ignore args.check = args.check or not any((args.diff, args.remove)) args.requirements = args.requirements or session.config.requirements # type: ignore args.gitignore = args.gitignore or session.config.gitignore # type: ignore args.sources.extend(session.config.sources) # type: ignore args.include.extend(session.config.include) # type: ignore args.exclude.extend(session.config.exclude) # type: ignore if args.gitignore: args.exclude.extend(get_exclude_list_from_gitignore()) include = re.compile("|".join(args.include)).pattern exclude = re.compile("|".join(args.exclude)).pattern unused_modules = set() packages: Set[str] = set() for source_path in args.sources: for py_path in session.list_paths(source_path, include, exclude): session.scanner.scan(source=session.read(py_path)[0]) unused_imports = session.scanner.unused_imports unused_modules.update({imp.name for imp in unused_imports}) packages.update( get_used_packages(session.scanner.imports, session.scanner.unused_imports)) if args.check: show(unused_imports, py_path) session.scanner.clear() if args.diff: exists_diff = print_if_exists(session.diff_file(py_path)) if args.permission and exists_diff: action = input( f"Apply suggested changes to '{color.paint(str(py_path), color.YELLOW)}' [Y/n/q] ? >" ).lower() if action == "q": return 1 elif actiontobool(action): args.remove = True if args.remove and session.refactor_file(py_path, apply=True)[1]: print( f"Refactoring '{color.paint(str(py_path), color.GREEN)}'") if not unused_modules and args.check: print( color.paint( "✨ Congratulations there is no unused import in your project. ✨", color.GREEN, )) if args.requirements and packages: for requirements in Path(".").glob("requirements*.txt"): splitlines_requirements = requirements.read_text().splitlines() result = splitlines_requirements.copy() for index, requirement in enumerate(splitlines_requirements): module_name = package_name_from_metadata( requirement.split("==")[0]) if module_name is None: if args.show_error: print( color.paint(requirement + " not found", color.RED)) continue if module_name not in packages: result.remove(requirement) if args.check: print( f"{color.paint(requirement, color.CYAN)} at " f"{color.paint(requirements.as_posix(), color.CYAN)}:{color.paint(str(index + 1), color.CYAN)}" ) if args.diff: exists_diff = print_if_exists( tuple( difflib.unified_diff( splitlines_requirements, result, fromfile=requirements.as_posix(), ))) if args.permission and exists_diff: action = input( f"Apply suggested changes to '{color.paint(requirements.as_posix(), color.CYAN)}' [Y/n/q] ? >" ).lower() if action == "q": return 1 if actiontobool(action): args.remove = True if args.remove: requirements.write_text("".join(result)) print( f"Refactoring '{color.paint(requirements.as_posix(), color.CYAN)}'" ) if unused_modules: return 1 else: return 0
def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( prog="unimport", description=C.DESCRIPTION, epilog="Get rid of all unused imports 🥳", ) exclusive_group = parser.add_mutually_exclusive_group(required=False) parser.add_argument( "sources", default=default_config.sources, nargs="*", help="Files and folders to find the unused imports.", action="store", type=Path, ) parser.add_argument( "--check", action="store_true", help="Prints which file the unused imports are in.", default=default_config.check, ) parser.add_argument( "-c", "--config", default=".", help="Read configuration from PATH.", metavar="PATH", action="store", type=Path, ) parser.add_argument( "--include", help="File include pattern.", metavar="include", action="store", default=default_config.include, type=str, ) parser.add_argument( "--exclude", help="File exclude pattern.", metavar="exclude", action="store", default=default_config.exclude, type=str, ) parser.add_argument( "--gitignore", action="store_true", help="Exclude .gitignore patterns. if present.", default=default_config.gitignore, ) parser.add_argument( "--ignore-init", action="store_true", help="Ignore the __init__.py file.", default=default_config.ignore_init, ) parser.add_argument( "--include-star-import", action="store_true", help="Include star imports during scanning and refactor.", default=default_config.include_star_import, ) parser.add_argument( "-d", "--diff", action="store_true", help="Prints a diff of all the changes unimport would make to a file.", default=default_config.diff, ) exclusive_group.add_argument( "-r", "--remove", action="store_true", help="Remove unused imports automatically.", default=default_config.remove, ) exclusive_group.add_argument( "-p", "--permission", action="store_true", help="Refactor permission after see diff.", default=default_config.permission, ) parser.add_argument( "--requirements", action="store_true", help= "Include requirements.txt file, You can use it with all other arguments", default=default_config.requirements, ) parser.add_argument( "-v", "--version", action="version", version=f"Unimport {C.VERSION}", help="Prints version of unimport", ) argv = argv if argv is not None else sys.argv[1:] args = parser.parse_args(argv) config = (Config(args.config).parse() if args.config and args.config.name in CONFIG_FILES else default_config) config = config.merge(**vars(args)) unused_modules = set() used_packages: Set[str] = set() for source_path in config.sources: for py_path in utils.list_paths(source_path, config.include, config.exclude): source, encoding = utils.read(py_path) analyzer = Analyzer( source=source, path=py_path, include_star_import=config.include_star_import, ) analyzer.traverse() unused_imports = list(analyzer.get_unused_imports()) unused_modules.update({imp.name for imp in unused_imports}) used_packages.update( utils.get_used_packages(analyzer.imports, unused_imports)) if config.check: for imp in unused_imports: if (isinstance(imp, ImportFrom) and imp.star and imp.suggestions): context = (color.paint( f"from {imp.name} import *", color.RED ) + " -> " + color.paint( f"from {imp.name} import {', '.join(imp.suggestions)}", color.GREEN, )) else: context = color.paint(imp.name, color.YELLOW) print(context + " at " + color.paint(py_path.as_posix(), color.GREEN) + ":" + color.paint(str(imp.lineno), color.GREEN)) if any((config.diff, config.remove)): refactor_result = refactor_string( source=source, unused_imports=unused_imports, ) if config.diff: diff = utils.diff( source=source, refactor_result=refactor_result, fromfile=py_path, ) exists_diff = bool(diff) if exists_diff: print(color.difference(diff)) if config.permission and exists_diff: action = input( f"Apply suggested changes to '{color.paint(str(py_path), color.YELLOW)}' [Y/n/q] ? >" ).lower() if action == "q": return 1 elif utils.actiontobool(action): config = config._replace(remove=True) if config.remove and source != refactor_result: py_path.write_text(refactor_result, encoding=encoding) print( f"Refactoring '{color.paint(str(py_path), color.GREEN)}'") analyzer.clear() if not unused_modules and config.check: print( color.paint( "✨ Congratulations there is no unused import in your project. ✨", color.GREEN, )) if config.requirements: for requirements in Path(".").glob("requirements*.txt"): source = requirements.read_text() copy_source = source.splitlines().copy() for index, requirement in enumerate(source.splitlines()): module_name = utils.package_name_from_metadata( requirement.split("==")[0]) if module_name is None: print(color.paint(requirement + " not found", color.RED)) continue if module_name not in used_packages: copy_source.remove(requirement) if config.check: print( f"{color.paint(requirement, color.CYAN)} at " f"{color.paint(requirements.as_posix(), color.CYAN)}:{color.paint(str(index + 1), color.CYAN)}" ) refactor_result = "\n".join(copy_source) if config.diff: diff = utils.diff( source=source, refactor_result=refactor_result, fromfile=requirements, ) exists_diff = bool(diff) if exists_diff: print(color.difference(diff)) if config.permission and exists_diff: action = input( f"Apply suggested changes to '{color.paint(requirements.as_posix(), color.CYAN)}' [Y/n/q] ? >" ).lower() if action == "q": return 1 if utils.actiontobool(action): config = config._replace(remove=True) if config.remove: requirements.write_text(refactor_result) print( f"Refactoring '{color.paint(requirements.as_posix(), color.CYAN)}'" ) if unused_modules: return 1 else: return 0
def test_use_color_setting_true(): text = "test text" action_text = paint(text, RED, True) assert RED + text + RESET == action_text
def test_use_color_setting_false(): text = "test text" action_text = paint(text, RED, False) assert text == action_text
def test_red_paint(): text = "test text" action_text = paint(text, RED) assert RED + text + RESET == action_text
def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( prog="unimport", description=C.DESCRIPTION, epilog=f"Get rid of all unused imports {emoji.PARTYING_FACE}", ) exclusive_group = parser.add_mutually_exclusive_group(required=False) options.add_sources_option(parser) options.add_check_option(parser) options.add_config_option(parser) color.add_color_option(parser) options.add_include_option(parser) options.add_exclude_option(parser) options.add_gitignore_option(parser) options.add_ignore_init_option(parser) options.add_include_star_import_option(parser) options.add_diff_option(parser) options.add_remove_option(exclusive_group) options.add_permission_option(exclusive_group) options.add_requirements_option(parser) options.add_version_option(parser) argv = argv if argv is not None else sys.argv[1:] args = parser.parse_args(argv) config = Config.get_config(args) unused_modules = set() used_packages: Set[str] = set() for path in config.get_paths(): source, encoding, newline = utils.read(path) with Analyzer( source=source, path=path, include_star_import=config.include_star_import, ): unused_imports = list( Import.get_unused_imports(config.include_star_import) ) unused_modules.update({imp.name for imp in unused_imports}) used_packages.update( utils.get_used_packages(Import.imports, unused_imports) ) if config.check: commands.check(path, unused_imports, args.color) if any((config.diff, config.remove)): refactor_result = refactor_string( source=source, unused_imports=unused_imports, ) if config.diff: exists_diff = commands.diff(path, source, refactor_result) if config.permission and exists_diff: commands.permission( path, encoding, newline, refactor_result, args.color ) if config.remove and source != refactor_result: commands.remove( path, encoding, newline, refactor_result, args.color ) if not unused_modules and config.check: print( color.paint( f"{emoji.STAR} Congratulations there is no unused import in your project. {emoji.STAR}", color.GREEN, args.color, ) ) if config.requirements: for path in config.get_requirements(): source = path.read_text() copy_source = source.splitlines().copy() for index, requirement in enumerate(source.splitlines()): module_name = utils.package_name_from_metadata( requirement.split("==")[0] ) if module_name is None: print( color.paint( requirement + " not found", color.RED, args.color ) ) continue if module_name not in used_packages: copy_source.remove(requirement) if config.check: commands.requirements_check( path, index, requirement, args.color ) refactor_result = "\n".join(copy_source) if config.diff: exists_diff = commands.diff(path, source, refactor_result) if config.permission and exists_diff: commands.requirements_permission( path, refactor_result, args.color ) if config.remove and source != refactor_result: commands.requirements_remove( path, refactor_result, args.color ) if unused_modules: return 1 else: return 0