示例#1
0
    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",
            )
示例#2
0
    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
示例#3
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)
示例#4
0
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