Skip to content

Commit

Permalink
✨ Add ruff support for python
Browse files Browse the repository at this point in the history
🐶 🐕 🦮 🐕‍🦺 🐩 🌭
  • Loading branch information
sakarias88 authored and abbec committed Feb 8, 2024
1 parent 49b3a31 commit bd68aa5
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 60 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Ruff support for python components. Currently have to opt in by
setting `defaultCheckPhase = "ruffStandardTests";` on
`base.languages.python`. This will change the global default for the
python language. To set it for a single component set
`installCheckPhase` to `standardTests` or `ruffStandardTests` or any
combination of the two. To individually set the formatter set the
`formatter` on the component to either "standard" (black + isort) or
to "ruff" (ruff 🐕). If formatter is not set it will be detected from the value of `installCheckPhase`.

## [3.0.0] - 2024-02-06

### Added
Expand Down
10 changes: 10 additions & 0 deletions docs/src/python/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ to add python linting to other types of components.

[^pylint]: ⚠️ Note that Nedryglot does not support pylint config in setup.cfg.

In case you want to use ruff instead of black, isort, pylint, and
flake8, you can set `installCheckPhase` to `ruffStandardTests`
instead.

#### Formatting

By default the formatter will follow the selected check variant (ruff
or black+isort). However, this can be overridden by setting the
`formatter` attribute to `ruff` or `standard`.


### Api docs
Nedryglot will output python API docs, by default using
Expand Down
12 changes: 12 additions & 0 deletions docs/src/python/override.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ nedryland.mkProject
];
}
```


# Changing the default checkphase

```nix
base.languages.python.override {
# Change the default tests and formatter to use ruff
defaultCheckPhase = "ruffStandardTests";
};
```

The above example changes the default tests and formatter to use ruff.
1 change: 1 addition & 0 deletions examples/hello/clients/hello/hello.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ base.languages.python.mkClient {
# Here we just use numpyWrapper since it's our own
# package and not part of the python version packages.
propagatedBuildInputs = [ numpyWrapper ];
installCheckPhase = "ruffStandardTests";
}
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions python/default.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
{ base, callPackage, lib, python, pythonVersions ? { } }:
{ base
, callPackage
, lib
, python
, pythonVersions ? { }
, defaultCheckPhase ? "standardTests"
}:
let
pythons = pythonVersions // { inherit python; };
defaultPythonName = "python";

hooks = callPackage ./hooks { };
hooks = callPackage ./hooks { inherit defaultCheckPhase; };

mkPackage = callPackage ./package.nix { inherit base; checkHook = hooks.check; };

Expand Down
135 changes: 104 additions & 31 deletions python/hooks/check.bash
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#! /usr/bin/env bash
# Shellcheck has some false positives
# seems to be multiple issues on github
# so ignore for now. Please retest later.
# shellcheck disable=SC2030,SC2031

printStatus() {
case "$1" in
Expand All @@ -14,64 +18,133 @@ printStatus() {
esac
}

runTool() {
local tool
tool="$1"
shift 1

local displayTool
displayTool="$1"
shift 1

local disableVar
disableVar="dontRun${tool^}"
local statusVar
statusVar="${tool}Status"

if [ -z "${!disableVar:-}" ] && [[ "$(command -v "$tool")" =~ ^/nix/store/.*$ ]]; then
echo -e "\n\x1b[1;36m${displayTool^}:\x1b[0m"
"$tool" "$@" 2>&1 | sed 's/^/ /'
declare -xg "$statusVar"=$?
else
echo "$tool is disabled."
fi
}

standardTests() (
# clean up after pip
rm -rf build/

set +e
echo -e "\n\x1b[1;36mBlack:\x1b[0m"
if [[ "$(command -v black)" =~ ^/nix/store/.*$ ]]; then
# shellcheck disable=SC2086
black ${blackArgs:-} --check . 2>&1 | sed 's/^/ /'
blackStatus=$?
else
echo " Black not supported on platform ${system:-unknown}."
fi
set -o pipefail

# shellcheck disable=SC2086
runTool black black ${blackArgs:-} --check .

echo -e "\n\x1b[1;36mIsort:\x1b[0m"
# shellcheck disable=SC2086
isort ${isortArgs:-} --check . 2>&1 | sed 's/^/ /'
isortStatus=$?
runTool isort isort ${isortArgs:-} --check .

echo -e "\n\x1b[1;36mPylint:\x1b[0m"
# shellcheck disable=SC2046,SC2086
HOME=$TMP pylint ${pylintArgs:-} --recursive=y . 2>&1 | sed 's/^/ /'
pylintStatus=$?
HOME=$TMP runTool pylint pylint ${pylintArgs:-} --recursive=y .

echo -e "\n\x1b[1;36mFlake8:\x1b[0m"
# shellcheck disable=SC2086
flake8 ${flake8Args:-} . 2>&1 | sed 's/^/ /'
flake8Status=$?
runTool flake8 flake8 ${flake8Args:-} .

echo -e "\n\x1b[1;36mMypy:\x1b[0m"
# shellcheck disable=SC2086
mypy ${mypyArgs:-} . 2>&1 | sed 's/^/ /'
mypyStatus=$?
runTool mypy mypy ${mypyArgs:-} .

echo -e "\n\x1b[1;36mPytest:\x1b[0m"
# shellcheck disable=SC2086
pytest ${pytestArgs:-} . 2>&1 | sed 's/^/ /'
pytestStatus=$?
runTool pytest pytest ${pytestArgs:-} .

# no tests ran
if [ $pytestStatus -eq 5 ]; then
if [ "${pytestStatus:-0}" -eq 5 ]; then
local pytestStatus
pytestStatus=0
fi

echo -e "Summary:
black: $(printStatus "${blackStatus:-skipped}")
isort: $(printStatus $isortStatus)
pylint: $(printStatus $pylintStatus)
flake8: $(printStatus $flake8Status)
mypy: $(printStatus $mypyStatus)
pytest: $(printStatus $pytestStatus)"
isort: $(printStatus "${isortStatus:-skipped}")
pylint: $(printStatus "${pylintStatus:-skipped}")
flake8: $(printStatus "${flake8Status:-skipped}")
mypy: $(printStatus "${mypyStatus:-skipped}")
pytest: $(printStatus "${pytestStatus:-skipped}")"

blackStatus=${blackStatus:-0}
: "${blackStatus:=0}" "${isortStatus:=0}" "${pylintStatus:=0}"
: "${flake8Status:=0}" "${mypyStatus:=0}" "${pytestStatus:=0}"
exit $((blackStatus + isortStatus + pylintStatus + flake8Status + mypyStatus + pytestStatus))
)

ruffStandardTests() (
# clean up after pip
rm -rf build/

set +e
set -o pipefail

# shellcheck disable=SC2086
runTool ruff "Ruff Check" check ${ruffArgs:-} .
local ruffCheckStatus
ruffCheckStatus=${ruffStatus:-}

# shellcheck disable=SC2086
runTool ruff "Ruff Format" format --diff ${ruffArgs:-} .
local ruffFormatStatus
ruffFormatStatus=${ruffStatus:-}

# shellcheck disable=SC2086
runTool mypy mypy ${mypyArgs:-} .

# shellcheck disable=SC2086
runTool pytest pytest ${pytestArgs:-} .

# no tests ran
if [ "${pytestStatus:-0}" -eq 5 ]; then
local pytestStatus
pytestStatus=0
fi

echo -e "Summary:
ruff check: $(printStatus "${ruffCheckStatus:-skipped}")
ruff format: $(printStatus "${ruffFormatStatus:-skipped}")
mypy: $(printStatus "${mypyStatus:-skipped}")
pytest: $(printStatus "${pytestStatus:-skipped}")"

: "${ruffCheckStatus:=0}" "${ruffFormatStatus:=0}"
: "${mypyStatus:=0}" "${pytestStatus:=0}"
exit $((ruffCheckStatus + ruffFormatStatus + mypyStatus + pytestStatus))
)

# If there is a checkPhase declared, mk-python-component in nixpkgs will put it in
# installCheckPhase so we use that phase as well (since this is executed later).
if [ -n "${doStandardTests-}" ] && [ -z "${installCheckPhase-}" ]; then
installCheckPhase=standardTests
installCheckPhase=@defaultCheckPhase@
fi

runStandardFormat() {
black . && isort .
}

runRuffFormat() {
ruff format .
}

runFormat() {
if [ "${formatter:-}" = "standard" ]; then
runStandardFormat
elif [ "${formatter:-}" = "ruff" ] || [[ "${installCheckPhase:-}" =~ "ruffStandardTests" ]]; then
runRuffFormat
else
runStandardFormat
fi
}
9 changes: 7 additions & 2 deletions python/hooks/config-merger.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ def change_header(config: dict, from_header: str, to_header: str) -> dict:
else:
return {}

return {to_header: sub_config}
header_sub_config = to_header.split(".")

for key in reversed(header_sub_config):
sub_config = {key: sub_config}

return sub_config


def parse_toml(config_file: str) -> dict:
Expand Down Expand Up @@ -192,7 +197,7 @@ def main():

with open(out_file, "w", encoding="utf-8") as output_file:
if out_file.suffix == ".toml":
toml.dump({"tool": combined_config}, output_file)
toml.dump(combined_config, output_file)
else:
config_parser = ConfigParser()
config_parser.read_dict(combined_config)
Expand Down
1 change: 1 addition & 0 deletions python/hooks/config/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[tool.ruff]
Loading

0 comments on commit bd68aa5

Please sign in to comment.