Skip to content
Snippets Groups Projects
autopep8_format_paths.py 5.99 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env python3
    # SPDX-License-Identifier: GPL-2.0-or-later
    
    """
    This script runs autopep8 on multiple files/directories.
    
    While it can be called directly, you may prefer to run this from Blender's root directory with the command:
    
       make format
    
    Otherwise you may call this script directly, for example:
    
       ./source/tools/utils_maintenance/autopep8_format_paths.py --changed-only tests/python
    """
    
    import argparse
    
    from typing import (
        List,
        Tuple,
        Optional,
    )
    
    
    VERSION_MIN = (1, 6, 0)
    VERSION_MAX_RECOMMENDED = (1, 6, 0)
    AUTOPEP8_FORMAT_CMD = "autopep8"
    
    BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
    os.chdir(BASE_DIR)
    
    
    extensions = (
        ".py",
    )
    
    ignore_files = {
        "release/scripts/modules/rna_manual_reference.py",  # Large generated file, don't format.
    }
    
    
    
    def compute_paths(paths: List[str], use_default_paths: bool) -> List[str]:
    
        # Optionally pass in files to operate on.
        if use_default_paths:
    
                if os.path.isdir(f) or (os.path.isfile(f) and f.endswith(extensions))
    
    
        if os.sep != "/":
            paths = [f.replace("/", os.sep) for f in paths]
        return paths
    
    
    
    def source_files_from_git(paths: List[str], changed_only: bool) -> List[str]:
    
        if changed_only:
            cmd = ("git", "diff", "HEAD", "--name-only", "-z", "--", *paths)
        else:
            cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z")
        files = subprocess.check_output(cmd).split(b'\0')
        return [f.decode('ascii') for f in files]
    
    
    
    def autopep8_ensure_version(autopep8_format_cmd_argument: str) -> Optional[Tuple[int, int, int]]:
    
        global AUTOPEP8_FORMAT_CMD
        autopep8_format_cmd = None
        version_output = None
    
        # Attempt to use `--autopep8-command` passed in from `make format`
    
        # so the autopep8 distributed with Blender will be used.
    
        for is_default in (True, False):
            if is_default:
                autopep8_format_cmd = autopep8_format_cmd_argument
    
                if autopep8_format_cmd and os.path.exists(autopep8_format_cmd):
                    pass
                else:
                    continue
            else:
                autopep8_format_cmd = "autopep8"
    
    
            cmd = [autopep8_format_cmd]
            if cmd[0].endswith(".py"):
                cmd = [sys.executable, *cmd]
    
    
                version_output = subprocess.check_output((*cmd, "--version")).decode('utf-8')
    
            except FileNotFoundError as e:
                continue
            AUTOPEP8_FORMAT_CMD = autopep8_format_cmd
            break
    
        if version_output is not None:
    
            version_str = next(iter(v for v in version_output.split() if v[0].isdigit()), None)
        if version_str is not None:
            # Ensure exactly 3 numbers.
            major, minor, patch = (tuple(int(n) for n in version_str.split("-")[0].split(".")) + (0, 0, 0))[0:3]
            print("Using %s (%d.%d.%d)..." % (AUTOPEP8_FORMAT_CMD, major, minor, patch))
            return major, minor, patch
        return None
    
    def autopep8_format(files: List[str]) -> bytes:
    
        cmd = [
            AUTOPEP8_FORMAT_CMD,
            # Operate on all directories recursively.
            "--recursive",
            # Update the files in-place.
            "--in-place",
            # Auto-detect the number of jobs to use.
            "--jobs=0",
        ] + files
    
    
        # Support executing from the module directory because Blender does not distribute the command.
        if cmd[0].endswith(".py"):
            cmd = [sys.executable, *cmd]
    
    
        return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
    
    
    
    def argparse_create() -> argparse.ArgumentParser:
    
        parser = argparse.ArgumentParser(
            description="Format Python source code.",
            epilog=__doc__,
            # Don't re-wrap text, keep newlines & indentation.
            formatter_class=argparse.RawTextHelpFormatter,
    
        parser.add_argument(
            "--changed-only",
            dest="changed_only",
            default=False,
            action='store_true',
            help=(
                "Format only edited files, including the staged ones. "
                "Using this with \"paths\" will pick the edited files lying on those paths. "
                "(default=False)"
            ),
            required=False,
        )
    
        parser.add_argument(
            "--autopep8-command",
            dest="autopep8_command",
    
            default=AUTOPEP8_FORMAT_CMD,
    
            help="The command to call autopep8.",
            required=False,
        )
    
        parser.add_argument(
            "paths",
            nargs=argparse.REMAINDER,
    
            help="All trailing arguments are treated as paths.",
    
    def main() -> None:
    
        args = argparse_create().parse_args()
    
        version = autopep8_ensure_version(args.autopep8_command)
    
        if version is None:
            print("Unable to detect 'autopep8 --version'")
            sys.exit(1)
        if version < VERSION_MIN:
            print("Version of autopep8 is too old:", version, "<", VERSION_MIN)
            sys.exit(1)
        if version > VERSION_MAX_RECOMMENDED:
            print(
                "WARNING: Version of autopep8 is too recent:",
                version, ">", VERSION_MAX_RECOMMENDED,
            )
            print(
                "You may want to install autopep8-%d.%d, "
                "or use the precompiled libs repository." %
                (VERSION_MAX_RECOMMENDED[0], VERSION_MAX_RECOMMENDED[1]),
            )
    
        use_default_paths = not (bool(args.paths) or bool(args.changed_only))
    
        paths = compute_paths(args.paths, use_default_paths)
        print("Operating on:" + (" (%d changed paths)" % len(paths) if args.changed_only else ""))
        for p in paths:
            print(" ", p)
    
        files = [
            f for f in source_files_from_git(paths, args.changed_only)
            if f.endswith(extensions)
            if f not in ignore_files
        ]
    
    
        # Happens when users run "make format" passing in individual C/C++ files
        # (and no Python files).
    
        autopep8_format(files)
    
    
    if __name__ == "__main__":
        main()