bowler/main.py (107 lines of code) (raw):

#!/usr/bin/env python3 # # Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. import importlib import importlib.util import logging import os.path import sys import unittest from importlib.abc import Loader from pathlib import Path from typing import List, cast import click from .query import Query from .tool import BowlerTool from .types import START, SYMBOL, TOKEN @click.group(invoke_without_command=True) @click.option("--debug/--quiet", default=False, help="Logging output level") @click.option("--version", "-V", is_flag=True, help="Print version string and exit") @click.pass_context def main(ctx: click.Context, debug: bool, version: bool) -> None: """Safe Python code modification and refactoring.""" if version: from bowler import __version__ click.echo(f"bowler {__version__}") return if debug: BowlerTool.NUM_PROCESSES = 1 BowlerTool.IN_PROCESS = True root = logging.getLogger() if not root.hasHandlers(): logging.addLevelName(logging.DEBUG, "DBG") logging.addLevelName(logging.INFO, "INF") logging.addLevelName(logging.WARNING, "WRN") logging.addLevelName(logging.ERROR, "ERR") level = logging.DEBUG if debug else logging.WARNING fmt = logging.Formatter("{levelname}:{filename}:{lineno} {message}", style="{") han = logging.StreamHandler(stream=sys.stderr) han.setFormatter(fmt) han.setLevel(level) root.setLevel(level) root.addHandler(han) if ctx.invoked_subcommand is None: return do(None, None) @main.command() @click.option("--selector-pattern", is_flag=True) @click.argument("paths", type=click.Path(exists=True), nargs=-1, required=False) def dump(selector_pattern: bool, paths: List[str]) -> None: """Dump the CST representation of each file in <paths>.""" return Query(paths).select_root().dump(selector_pattern).retcode @main.command() @click.option("-i", "--interactive", is_flag=True) @click.argument("query", required=False) @click.argument("paths", type=click.Path(exists=True), nargs=-1, required=False) def do(interactive: bool, query: str, paths: List[str]) -> None: """Execute a query or enter interactive mode.""" if not query or query == "-": namespace = {"Query": Query, "START": START, "SYMBOL": SYMBOL, "TOKEN": TOKEN} try: import IPython IPython.start_ipython(argv=[], user_ns=namespace) except ImportError: import code as _code _code.interact(local=namespace) finally: return code = compile(query, "<console>", "eval") result = eval(code) # noqa eval() - developer tool, hopefully they're not dumb if isinstance(result, Query): if result.retcode: exc = click.ClickException("query failed") exc.exit_code = result.retcode raise exc result.diff(interactive=interactive) elif result: click.echo(repr(result)) @main.command() @click.argument("codemod", required=True, type=str) @click.argument("argv", required=False, type=str, nargs=-1) def run(codemod: str, argv: List[str]) -> None: """ Execute a file-based code modification. Takes either a path to a python script, or an importable module name, and attempts to import and run a "main()" function from that script/module if found. Extra arguments to this command will be supplied to the script/module. Use `--` to forcibly pass through all following options or arguments. """ try: original_argv = sys.argv[1:] sys.argv[1:] = argv path = Path(codemod) if path.exists(): if path.is_dir(): raise click.ClickException("running directories not supported") spec = importlib.util.spec_from_file_location( # type: ignore path.name, path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore else: module = importlib.import_module(codemod) main = getattr(module, "main", None) if main is not None: main() except ImportError as e: raise click.ClickException(f"failed to import codemod: {e}") from e finally: sys.argv[1:] = original_argv @main.command() @click.argument("codemod", required=True, type=str) def test(codemod: str) -> None: """ Run the tests in the codemod file """ # TODO: Unify the import code between 'run' and 'test' module_name_from_codemod = os.path.basename(codemod).replace(".py", "") spec = importlib.util.spec_from_file_location(module_name_from_codemod, codemod) foo = importlib.util.module_from_spec(spec) cast(Loader, spec.loader).exec_module(foo) suite = unittest.TestLoader().loadTestsFromModule(foo) result = unittest.TextTestRunner().run(suite) sys.exit(not result.wasSuccessful()) if __name__ == "__main__": main()