def test_leaky_codemod(self) -> None: with temp_workspace() as tmp: # File to trigger codemod example: Path = tmp / "example.py" example.write_text("""print("Hello")""") # File that should not be modified other = tmp / "other.py" other.touch() # Run command command_instance = PrintToPPrintCommand(CodemodContext()) files = gather_files(".") result = parallel_exec_transform_with_prettyprint( command_instance, files, format_code=False, hide_progress=True, ) # Check results self.assertEqual(2, result.successes) self.assertEqual(0, result.skips) self.assertEqual(0, result.failures) # Expect example.py to be modified self.assertIn( "from pprint import pprint", example.read_text(), "import missing in example.py", ) # Expect other.py to NOT be modified self.assertNotIn( "from pprint import pprint", other.read_text(), "import found in other.py", )
def codemod(path): """`hypothesis codemod` refactors deprecated or inefficient code. It adapts `python -m libcst.tool`, removing many features and config options which are rarely relevant for this purpose. If you need more control, we encourage you to use the libcst CLI directly; if not this one is easier. PATH is the file(s) or directories of files to format in place, or "-" to read from stdin and write to stdout. """ try: from libcst.codemod import gather_files from hypothesis.extra import codemods except ImportError: sys.stderr.write( "You are missing required dependencies for this option. Run:\n\n" " python -m pip install --upgrade hypothesis[codemods]\n\n" "and try again." ) sys.exit(1) # Special case for stdin/stdout usage if "-" in path: if len(path) > 1: raise Exception( "Cannot specify multiple paths when reading from stdin!" ) print("Codemodding from stdin", file=sys.stderr) print(codemods.refactor(sys.stdin.read())) return 0 # Find all the files to refactor, and then codemod them files = gather_files(path) errors = set() if len(files) <= 1: errors.add(_refactor(codemods.refactor, *files)) else: with Pool() as pool: for msg in pool.imap_unordered( partial(_refactor, codemods.refactor), files ): errors.add(msg) errors.discard(None) for msg in errors: print(msg, file=sys.stderr) return 1 if errors else 0
def call_command(command_instance: BaseCodemodCommand, path: str): """Call libCST with our customized command.""" files = gather_files(path) try: # Super simplified call result = parallel_exec_transform_with_prettyprint( command_instance, files, # Number of jobs to use when processing files. Defaults to number of cores jobs=None, ) except KeyboardInterrupt: raise click.Abort("Interrupted!") # fancy summary a-la libCST total = result.successes + result.skips + result.failures click.echo(f"Finished codemodding {total} files!") click.echo(f" - Transformed {result.successes} files successfully.") click.echo(f" - Skipped {result.skips} files.") click.echo(f" - Failed to codemod {result.failures} files.") click.echo(f" - {result.warnings} warnings were generated.") if result.failures > 0: raise click.exceptions.Exit(1)
def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901 # Grab the configuration for running this, if it exsts. config = _find_and_load_config(proc_name) # First, try to grab the command with a first pass. We aren't going to react # to user input here, so refuse to add help. Help will be parsed in the # 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) args, _ = parser.parse_known_args(command_args) # Now, try to load the class and get its arguments for help purposes. if args.command is not None: command_path = args.command.split(".") if len(command_path) < 2: print(f"{args.command} is not a valid codemod command", file=sys.stderr) return 1 command_module_name, command_class_name = ( ".".join(command_path[:-1]), command_path[-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, ) return 1 else: # Dummy, specifically to allow for running --help with no arguments. command_class = CodemodCommand # Now, construct the full parser, parse the args and run the class. parser = argparse.ArgumentParser( description=("Execute a codemod against a series of files." if command_class is CodemodCommand else command_class.DESCRIPTION), prog=f"{proc_name} codemod", fromfile_prefix_chars="@", ) parser.add_argument( "command", metavar="COMMAND", type=str, help= ("The name of the file (minus the path and extension) and class joined with " + "a '.' that defines your command (e.g. strip_strings_from_types.StripStringsCommand)" ), ) parser.add_argument( "path", metavar="PATH", nargs="+", help= ("Path to codemod. Can be a directory, file, or multiple of either. To " + 'instead read from stdin and write to stdout, use "-"'), ) parser.add_argument( "-j", "--jobs", metavar="JOBS", help= "Number of jobs to use when processing files. Defaults to number of cores", type=int, default=None, ) parser.add_argument( "-p", "--python-version", metavar="VERSION", help= ("Override the version string used for parsing Python source files. Defaults " + "to the version of python used to run this tool."), type=str, default=None, ) parser.add_argument( "-u", "--unified-diff", metavar="CONTEXT", help= "Output unified diff instead of contents. Implies outputting to stdout", type=int, nargs="?", default=None, const=5, ) parser.add_argument("--include-generated", action="store_true", help="Codemod generated files.") parser.add_argument("--include-stubs", action="store_true", help="Codemod typing stub files.") parser.add_argument( "--no-format", action="store_true", help="Don't format resulting codemod with configured formatter.", ) parser.add_argument( "--show-successes", action="store_true", help="Print files successfully codemodded with no warnings.", ) parser.add_argument( "--hide-generated-warnings", action="store_true", help="Do not print files that are skipped for being autogenerated.", ) parser.add_argument( "--hide-blacklisted-warnings", action="store_true", help="Do not print files that are skipped for being blacklisted.", ) parser.add_argument( "--hide-progress", action="store_true", help= "Do not print progress indicator. Useful if calling from a script.", ) command_class.add_args(parser) args = parser.parse_args(command_args) codemod_args = { k: v for k, v in vars(args).items() if k not in [ "command", "path", "unified_diff", "jobs", "python_version", "include_generated", "include_stubs", "no_format", "show_successes", "hide_generated_warnings", "hide_blacklisted_warnings", "hide_progress", ] } command_instance = command_class(CodemodContext(), **codemod_args) # Special case for allowing stdin/stdout. Note that this does not allow for # full-repo metadata since there is no path. if any(p == "-" for p in args.path): if len(args.path) > 1: raise Exception( "Cannot specify multiple paths when reading from stdin!") print("Codemodding from stdin", file=sys.stderr) oldcode = sys.stdin.read() newcode = exec_transform_with_prettyprint( command_instance, oldcode, include_generated=args.include_generated, generated_code_marker=config["generated_code_marker"], format_code=not args.no_format, formatter_args=config["formatter"], python_version=args.python_version, ) if not newcode: print("Failed to codemod from stdin", file=sys.stderr) return 1 # Now, either print or diff the code if args.unified_diff: print( diff_code(oldcode, newcode, args.unified_diff, filename="stdin")) else: print(newcode) return 0 # Let's run it! files = gather_files(args.path, include_stubs=args.include_stubs) try: result = parallel_exec_transform_with_prettyprint( command_instance, files, jobs=args.jobs, unified_diff=args.unified_diff, include_generated=args.include_generated, generated_code_marker=config["generated_code_marker"], format_code=not args.no_format, formatter_args=config["formatter"], show_successes=args.show_successes, hide_generated=args.hide_generated_warnings, hide_blacklisted=args.hide_blacklisted_warnings, hide_progress=args.hide_progress, blacklist_patterns=config["blacklist_patterns"], python_version=args.python_version, repo_root=config["repo_root"], ) except KeyboardInterrupt: print("Interrupted!", file=sys.stderr) return 2 # Print a fancy summary at the end. print( f"Finished codemodding {result.successes + result.skips + result.failures} files!", file=sys.stderr, ) print(f" - Transformed {result.successes} files successfully.", file=sys.stderr) print(f" - Skipped {result.skips} files.", file=sys.stderr) print(f" - Failed to codemod {result.failures} files.", file=sys.stderr) print(f" - {result.warnings} warnings were generated.", file=sys.stderr) return 1 if result.failures > 0 else 0