Skip to content

Commit

Permalink
Allow running codemods without configuring in YAML
Browse files Browse the repository at this point in the history
This enables codemodding things by just plonking a CodemodCommand class
into any old importable module and running
`python -m libcst.tool codemod -x some_module.SomeClass ...`
  • Loading branch information
akx committed Mar 23, 2023
1 parent 8946f8b commit 23d78e0
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 28 deletions.
19 changes: 19 additions & 0 deletions libcst/codemod/tests/test_codemod_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import platform
import subprocess
import sys
from pathlib import Path
from unittest import skipIf

Expand Down Expand Up @@ -44,3 +45,21 @@ def test_codemod_formatter_error_input(self) -> None:
"error: cannot format -: Cannot parse: 13:10: async with AsyncExitStack() as stack:",
rlt.stderr.decode("utf-8"),
)

def test_codemod_external(self) -> None:
# Test running the NOOP command as an "external command"
# against this very file.
output = subprocess.check_output(
[
sys.executable,
"-m",
"libcst.tool",
"codemod",
"-x", # external module
"libcst.codemod.commands.noop.NOOPCommand",
str(Path(__file__)),
],
encoding="utf-8",
stderr=subprocess.STDOUT,
)
assert "Finished codemodding 1 files!" in output
73 changes: 45 additions & 28 deletions libcst/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901
# full parser below once we know the command and have added its arguments.
parser = argparse.ArgumentParser(add_help=False, fromfile_prefix_chars="@")
parser.add_argument("command", metavar="COMMAND", type=str, nargs="?", default=None)
ext_action = parser.add_argument(
"-x",
"--external",
action="store_true",
default=False,
help="Interpret `command` as just a module/class specifier",
)
args, _ = parser.parse_known_args(command_args)

# Now, try to load the class and get its arguments for help purposes.
Expand All @@ -399,26 +406,34 @@ def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901
if not (command_module_name and command_class_name):
print(f"{args.command} is not a valid codemod command", file=sys.stderr)
return 1
command_class = None
for module in config["modules"]:
try:
command_class = getattr(
importlib.import_module(f"{module}.{command_module_name}"),
command_class_name,
)
break
# Only swallow known import errors, show the rest of the exceptions
# to the user who is trying to run the codemod.
except AttributeError:
continue
except ModuleNotFoundError:
continue
if command_class is None:
print(
f"Could not find {command_module_name} in any configured modules",
file=sys.stderr,
if args.external:
# There's no error handling here on purpose; if the user opted in for `-x`,
# they'll probably want to see the exact import error too.
command_class = getattr(
importlib.import_module(command_module_name),
command_class_name,
)
return 1
else:
command_class = None
for module in config["modules"]:
try:
command_class = getattr(
importlib.import_module(f"{module}.{command_module_name}"),
command_class_name,
)
break
# Only swallow known import errors, show the rest of the exceptions
# to the user who is trying to run the codemod.
except AttributeError:
continue
except ModuleNotFoundError:
continue
if command_class is None:
print(
f"Could not find {command_module_name} in any configured modules",
file=sys.stderr,
)
return 1
else:
# Dummy, specifically to allow for running --help with no arguments.
command_class = CodemodCommand
Expand All @@ -433,6 +448,7 @@ def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901
prog=f"{proc_name} codemod",
fromfile_prefix_chars="@",
)
parser._add_action(ext_action)
parser.add_argument(
"command",
metavar="COMMAND",
Expand Down Expand Up @@ -518,20 +534,21 @@ def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901
k: v
for k, v in vars(args).items()
if k
not in [
not in {
"command",
"path",
"unified_diff",
"jobs",
"python_version",
"external",
"hide_blacklisted_warnings",
"hide_generated_warnings",
"hide_progress",
"include_generated",
"include_stubs",
"jobs",
"no_format",
"path",
"python_version",
"show_successes",
"hide_generated_warnings",
"hide_blacklisted_warnings",
"hide_progress",
]
"unified_diff",
}
}
command_instance = command_class(CodemodContext(), **codemod_args)

Expand Down

0 comments on commit 23d78e0

Please sign in to comment.