Skip to content
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ inputs:
description: 'Python Version specifier (PEP440) - e.g. "21.5b1"'
required: false
default: ""
use_pyproject:
description: Read black version specifier from pyproject.toml if `true`.
required: false
default: "false"
summary:
description: "Whether to add the output to the workflow summary"
required: false
Expand Down Expand Up @@ -70,5 +74,6 @@ runs:
INPUT_JUPYTER: ${{ inputs.jupyter }}
INPUT_BLACK_ARGS: ${{ inputs.black_args }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_USE_PYPROJECT: ${{ inputs.use_pyproject }}
pythonioencoding: utf-8
shell: bash
93 changes: 90 additions & 3 deletions action/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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"
Expand All @@ -13,12 +15,97 @@
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"\[.*\]")


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)

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":
return ""
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 = VERSION
if VERSION and VERSION[0] in "0123456789":
version_specifier = f"=={VERSION}"
version_specifier = determine_version_specifier()
if JUPYTER:
extra_deps = "[colorama,jupyter]"
else:
Expand Down
17 changes: 14 additions & 3 deletions docs/integrations/github_actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ We recommend the use of the `@stable` tag, but per version tags also exist if yo
that. Note that the action's version you select is independent of the version of _Black_
the action will use.

The version of _Black_ the action will use can be configured via `version`. This can be
any
The version of _Black_ the action will use can be configured via `version` or read from
the pyproject.toml file. `version` can be any
[valid version specifier](https://packaging.python.org/en/latest/glossary/#term-Version-Specifier)
or just the version number if you want an exact version. The action defaults to the
or just the version number if you want an exact version. To read the version from the
pyproject.toml file instead, set `use_pyproject` to `true`. The action defaults to the
latest release available on PyPI. Only versions available from PyPI are supported, so no
commit SHAs or branch names.

Expand Down Expand Up @@ -70,3 +71,13 @@ If you want to match versions covered by Black's
src: "./src"
version: "~= 22.0"
```

If you want to read the version from pyproject.toml, set `use_pyproject` to `true`:

```yaml
- uses: psf/black@stable
with:
options: "--check --verbose"
src: "./src"
use_pyproject: true
```