182 lines
5.4 KiB
Python
182 lines
5.4 KiB
Python
import os
|
|
import re
|
|
import shlex
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from subprocess import PIPE, STDOUT, run
|
|
from typing import Union
|
|
|
|
ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"])
|
|
ENV_PATH = ACTION_PATH / ".black-env"
|
|
ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin")
|
|
OPTIONS = os.getenv("INPUT_OPTIONS", default="")
|
|
SRC = os.getenv("INPUT_SRC", default="")
|
|
JUPYTER = os.getenv("INPUT_JUPYTER") == "true"
|
|
BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="")
|
|
VERSION = os.getenv("INPUT_VERSION", default="")
|
|
USE_PYPROJECT = os.getenv("INPUT_USE_PYPROJECT") == "true"
|
|
|
|
BLACK_VERSION_RE = re.compile(r"^black([^A-Z0-9._-]+.*)$", re.IGNORECASE)
|
|
EXTRAS_RE = re.compile(r"\[.*\]")
|
|
EXPORT_SUBST_FAIL_RE = re.compile(r"\$Format:.*\$")
|
|
|
|
|
|
def determine_version_specifier() -> str:
|
|
"""Determine the version of Black to install.
|
|
|
|
The version can be specified either via the `with.version` input or via the
|
|
pyproject.toml file if `with.use_pyproject` is set to `true`.
|
|
"""
|
|
if USE_PYPROJECT and VERSION:
|
|
print(
|
|
"::error::'with.version' and 'with.use_pyproject' inputs are "
|
|
"mutually exclusive.",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
sys.exit(1)
|
|
if USE_PYPROJECT:
|
|
return read_version_specifier_from_pyproject()
|
|
elif VERSION and VERSION[0] in "0123456789":
|
|
return f"=={VERSION}"
|
|
else:
|
|
return VERSION
|
|
|
|
|
|
def read_version_specifier_from_pyproject() -> str:
|
|
if sys.version_info < (3, 11):
|
|
print(
|
|
"::error::'with.use_pyproject' input requires Python 3.11 or later.",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
sys.exit(1)
|
|
|
|
import tomllib # type: ignore[import-not-found,unreachable]
|
|
|
|
try:
|
|
with Path("pyproject.toml").open("rb") as fp:
|
|
pyproject = tomllib.load(fp)
|
|
except FileNotFoundError:
|
|
print(
|
|
"::error::'with.use_pyproject' input requires a pyproject.toml file.",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
sys.exit(1)
|
|
|
|
version = pyproject.get("tool", {}).get("black", {}).get("required-version")
|
|
if version is not None:
|
|
return f"=={version}"
|
|
|
|
arrays = [
|
|
pyproject.get("project", {}).get("dependencies"),
|
|
*pyproject.get("project", {}).get("optional-dependencies", {}).values(),
|
|
]
|
|
for array in arrays:
|
|
version = find_black_version_in_array(array)
|
|
if version is not None:
|
|
break
|
|
|
|
if version is None:
|
|
print(
|
|
"::error::'black' dependency missing from pyproject.toml.",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
sys.exit(1)
|
|
|
|
return version
|
|
|
|
|
|
def find_black_version_in_array(array: object) -> Union[str, None]:
|
|
if not isinstance(array, list):
|
|
return None
|
|
try:
|
|
for item in array:
|
|
# Rudimentary PEP 508 parsing.
|
|
item = item.split(";")[0]
|
|
item = EXTRAS_RE.sub("", item).strip()
|
|
if item == "black":
|
|
print(
|
|
"::error::Version specifier missing for 'black' dependency in "
|
|
"pyproject.toml.",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
sys.exit(1)
|
|
elif m := BLACK_VERSION_RE.match(item):
|
|
return m.group(1).strip()
|
|
except TypeError:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)
|
|
|
|
version_specifier = determine_version_specifier()
|
|
if JUPYTER:
|
|
extra_deps = "[colorama,jupyter]"
|
|
else:
|
|
extra_deps = "[colorama]"
|
|
if version_specifier:
|
|
req = f"black{extra_deps}{version_specifier}"
|
|
else:
|
|
describe_name = ""
|
|
with open(ACTION_PATH / ".git_archival.txt", encoding="utf-8") as fp:
|
|
for line in fp:
|
|
if line.startswith("describe-name: "):
|
|
describe_name = line[len("describe-name: ") :].rstrip()
|
|
break
|
|
if not describe_name:
|
|
print("::error::Failed to detect action version.", file=sys.stderr, flush=True)
|
|
sys.exit(1)
|
|
# expected format is one of:
|
|
# - 23.1.0
|
|
# - 23.1.0-51-g448bba7
|
|
# - $Format:%(describe:tags=true,match=*[0-9]*)$ (if export-subst fails)
|
|
if (
|
|
describe_name.count("-") < 2
|
|
and EXPORT_SUBST_FAIL_RE.match(describe_name) is None
|
|
):
|
|
# the action's commit matches a tag exactly, install exact version from PyPI
|
|
req = f"black{extra_deps}=={describe_name}"
|
|
else:
|
|
# the action's commit does not match any tag, install from the local git repo
|
|
req = f".{extra_deps}"
|
|
print(f"Installing {req}...", flush=True)
|
|
pip_proc = run(
|
|
[str(ENV_BIN / "python"), "-m", "pip", "install", req],
|
|
stdout=PIPE,
|
|
stderr=STDOUT,
|
|
encoding="utf-8",
|
|
cwd=ACTION_PATH,
|
|
)
|
|
if pip_proc.returncode:
|
|
print(pip_proc.stdout)
|
|
print("::error::Failed to install Black.", file=sys.stderr, flush=True)
|
|
sys.exit(pip_proc.returncode)
|
|
|
|
|
|
base_cmd = [str(ENV_BIN / "black")]
|
|
if BLACK_ARGS:
|
|
# TODO: remove after a while since this is deprecated in favour of SRC + OPTIONS.
|
|
proc = run(
|
|
[*base_cmd, *shlex.split(BLACK_ARGS)],
|
|
stdout=PIPE,
|
|
stderr=STDOUT,
|
|
encoding="utf-8",
|
|
)
|
|
else:
|
|
proc = run(
|
|
[*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)],
|
|
stdout=PIPE,
|
|
stderr=STDOUT,
|
|
encoding="utf-8",
|
|
)
|
|
shutil.rmtree(ENV_PATH, ignore_errors=True)
|
|
print(proc.stdout)
|
|
sys.exit(proc.returncode)
|