Rename modules in `./scripts/modules/` to use an underscore prefix to make it clear they aren't intended to be part of public API's. This also means there is no implication that these modules should be stable, allowing us to change them based on Blender's internal usage. The following modules have been marked as private: - `animsys_refactor` - `bl_console_utils` - `bl_i18n_utils` - `bl_previews_utils` - `bl_rna_utils` - `bl_text_utils` - `bl_ui_utils` - `bpy_restrict_state` - `console_python` - `console_shell` - `graphviz_export` - `keyingsets_utils` - `rna_info` - `rna_manual_reference` - `rna_xml` Note that we could further re-arrange these modules (under `_bpy_internal` in some cases), this change is mainly to mark them as private, further changes can be handed on a case-by-case basis. Ref !147773
308 lines
9.0 KiB
Python
Executable File
308 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
#
|
|
# 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:
|
|
|
|
./tools/utils_maintenance/autopep8_format_paths.py --changed-only tests/python
|
|
"""
|
|
|
|
__all__ = (
|
|
"main",
|
|
)
|
|
|
|
import os
|
|
import sys
|
|
|
|
import subprocess
|
|
import argparse
|
|
|
|
VERSION_MIN = (2, 3, 1)
|
|
VERSION_MAX_RECOMMENDED = (2, 3, 1)
|
|
AUTOPEP8_FORMAT_CMD = "autopep8"
|
|
AUTOPEP8_FORMAT_DEFAULT_ARGS = (
|
|
# Operate on all directories recursively.
|
|
"--recursive",
|
|
# Update the files in-place.
|
|
"--in-place",
|
|
# Auto-detect the number of jobs to use.
|
|
"--jobs=0",
|
|
)
|
|
|
|
BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
os.chdir(BASE_DIR)
|
|
|
|
|
|
extensions = (
|
|
".py",
|
|
)
|
|
|
|
ignore_files = {
|
|
"scripts/modules/_rna_manual_reference.py", # Large generated file, don't format.
|
|
"tools/svn_rev_map/rev_to_sha1.py",
|
|
"tools/svn_rev_map/sha1_to_rev.py",
|
|
}
|
|
|
|
|
|
def compute_paths(paths: list[str], use_default_paths: bool) -> list[str]:
|
|
# Optionally pass in files to operate on.
|
|
if use_default_paths:
|
|
paths = [
|
|
"build_files",
|
|
"intern",
|
|
"release",
|
|
"scripts",
|
|
"doc",
|
|
"source",
|
|
"tests",
|
|
"tools",
|
|
]
|
|
else:
|
|
paths = [
|
|
f for f in 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('utf-8') for f in files]
|
|
|
|
|
|
def autopep8_parse_version(version: str) -> tuple[int, int, int]:
|
|
# Ensure exactly 3 numbers.
|
|
major, minor, patch = (tuple(int(n) for n in version.split("-")[0].split(".")) + (0, 0, 0))[0:3]
|
|
return major, minor, patch
|
|
|
|
|
|
def version_str_from_tuple(version: tuple[int, ...]) -> str:
|
|
return ".".join(str(x) for x in version)
|
|
|
|
|
|
def autopep8_ensure_version_from_command(
|
|
autopep8_format_cmd_argument: str,
|
|
) -> tuple[str, tuple[int, int, int]] | None:
|
|
|
|
# The version to parse.
|
|
version_str: str | None = None
|
|
|
|
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]
|
|
|
|
try:
|
|
version_output = subprocess.check_output((*cmd, "--version")).decode('utf-8')
|
|
except FileNotFoundError:
|
|
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:
|
|
assert isinstance(autopep8_format_cmd, str)
|
|
major, minor, patch = autopep8_parse_version(version_str)
|
|
return autopep8_format_cmd, (major, minor, patch)
|
|
|
|
return None
|
|
|
|
|
|
def autopep8_ensure_version_from_module() -> tuple[str, tuple[int, int, int]] | None:
|
|
|
|
# The version to parse.
|
|
version_str: str | None = None
|
|
|
|
# Extract the version from the module.
|
|
try:
|
|
# pylint: disable-next=import-outside-toplevel
|
|
import autopep8 # type: ignore
|
|
except ModuleNotFoundError as ex:
|
|
if ex.name != "autopep8":
|
|
raise ex
|
|
return None
|
|
|
|
version_str = autopep8.__version__
|
|
if version_str is not None:
|
|
major, minor, patch = autopep8_parse_version(version_str)
|
|
return autopep8.__file__, (major, minor, patch)
|
|
|
|
return None
|
|
|
|
|
|
def autopep8_format(files: list[str]) -> bytes:
|
|
cmd = [
|
|
AUTOPEP8_FORMAT_CMD,
|
|
*AUTOPEP8_FORMAT_DEFAULT_ARGS,
|
|
*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 autopep8_format_no_subprocess(files: list[str]) -> None:
|
|
cmd = [
|
|
*AUTOPEP8_FORMAT_DEFAULT_ARGS,
|
|
*files
|
|
]
|
|
# NOTE: this import will have already succeeded, see: `autopep8_ensure_version_from_module`.
|
|
# pylint: disable-next=import-outside-toplevel
|
|
from autopep8 import main as autopep8_main
|
|
autopep8_main(argv=cmd)
|
|
|
|
|
|
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(
|
|
"--no-subprocess",
|
|
dest="no_subprocess",
|
|
default=False,
|
|
action='store_true',
|
|
help=(
|
|
"Don't use a sub-process, load autopep8 into this instance of Python. "
|
|
"Works around 8191 argument length limit on WIN32."
|
|
),
|
|
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.",
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
args = argparse_create().parse_args()
|
|
|
|
if args.no_subprocess:
|
|
source_and_version = autopep8_ensure_version_from_module()
|
|
else:
|
|
source_and_version = autopep8_ensure_version_from_command(args.autopep8_command)
|
|
|
|
if source_and_version is None:
|
|
if args.no_subprocess:
|
|
sys.stderr.write("ERROR: unable to import \"autopep8\" from Python at \"{:s}\".\n".format(sys.executable))
|
|
else:
|
|
sys.stderr.write("ERROR: unable to detect \"autopep8 --version\".\n")
|
|
|
|
sys.stderr.write("You may want to install autopep8-{:s}, or use the pre-compiled libs repository.\n".format(
|
|
version_str_from_tuple(VERSION_MAX_RECOMMENDED[0:2]),
|
|
))
|
|
return 1
|
|
|
|
autopep8_source, version = source_and_version
|
|
|
|
if version < VERSION_MIN:
|
|
sys.stderr.write("Using \"{:s}\"\nERROR: the autopep8 version is too old: {:s} < {:s}.\n".format(
|
|
autopep8_source,
|
|
version_str_from_tuple(version),
|
|
version_str_from_tuple(VERSION_MIN),
|
|
))
|
|
return 1
|
|
if version > VERSION_MAX_RECOMMENDED:
|
|
sys.stderr.write("Using \"{:s}\"\nWARNING: the autopep8 version is too recent: {:s} > {:s}.\n".format(
|
|
autopep8_source,
|
|
version_str_from_tuple(version),
|
|
version_str_from_tuple(VERSION_MAX_RECOMMENDED),
|
|
))
|
|
sys.stderr.write("You may want to install autopep8-{:s}, or use the pre-compiled libs repository.\n".format(
|
|
version_str_from_tuple(VERSION_MAX_RECOMMENDED[0:2]),
|
|
))
|
|
else:
|
|
print("Using \"{:s}\", ({:s})...".format(autopep8_source, version_str_from_tuple(version)))
|
|
|
|
use_default_paths = not (bool(args.paths) or bool(args.changed_only))
|
|
paths = compute_paths(args.paths, use_default_paths)
|
|
# Check if user-defined paths exclude all Python sources.
|
|
if args.paths and not paths:
|
|
print("Skip autopep8: no target to format")
|
|
return 0
|
|
|
|
print("Operating on:" + (" ({:d} changed paths)".format(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).
|
|
if not files:
|
|
return 0
|
|
|
|
if args.no_subprocess:
|
|
autopep8_format_no_subprocess(files)
|
|
else:
|
|
autopep8_format(files)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|