
"dependency-groups" is the mechanism for storing package requirements in `pyproject.toml`, recommended for formatting tools (see https://packaging.python.org/en/latest/specifications/dependency-groups/ ) this change allow the black action to look also in those locations when determining the version of black to install
183 lines
5.5 KiB
Python
183 lines
5.5 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("dependency-groups", {}).values(),
|
|
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)
|