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()