Skip to content

Instantly share code, notes, and snippets.

@RascalTwo
Last active January 12, 2024 23:20
Show Gist options
  • Save RascalTwo/5f658cbaa87c4c4aecf47e98ebccb9f1 to your computer and use it in GitHub Desktop.
Save RascalTwo/5f658cbaa87c4c4aecf47e98ebccb9f1 to your computer and use it in GitHub Desktop.
Helper script to make ordering of pre-commit hooks a breeze!

Reorder pre-commit hooks

A simple script that allows you to reorder your pre-commit hooks by ID.

Simply run the script - with custom --input and --output if needed - and it will write the IDs of hooks to order.txt - or --temp-file.

Then after modifying the order of the IDs in order.txt, hit enter in the script and it will reorder the hooks in the file you specified.

from __future__ import annotations
import yaml
import sys
from typing import TypedDict
import argparse
class Args(TypedDict):
dry: bool
input: str
temp_order_file: str
output: str
def parse_args() -> Args:
parser = argparse.ArgumentParser()
parser.add_argument(
"--dry",
help="Dry run",
action="store_true",
default=False,
)
parser.add_argument(
"--input",
help="Input file",
default="./.pre-commit-config.yaml",
)
parser.add_argument(
"--temp-order-file",
help="Temp order file",
default="./hook-id-order.txt",
)
parser.add_argument(
"--output",
help="Output file",
default="./.pre-commit-config.yaml",
)
args = parser.parse_args(sys.argv[1:])
return Args(
dry=args.dry,
input=args.input,
temp_order_file=args.temp_order_file,
output=args.output,
)
class PreCommitHook(TypedDict, total=False):
id: str
args: list[str]
name: None | str
description: None | str
language: None | str
types: None | list[str]
pass_filenames: None | bool
entry: None | str
class PreCommitRepo(TypedDict, total=False):
repo: str
rev: None | str
hooks: list[PreCommitHook]
additional_dependencies: None | list[str]
def are_repos_equal(repo1: PreCommitRepo, repo2: PreCommitRepo) -> bool:
return repo1["repo"] == repo2["repo"] and repo1.get("rev", None) == repo2.get(
"rev", None
)
class PreCommitConfig(TypedDict):
repos: list[PreCommitRepo]
def load_config(input_file: str) -> PreCommitConfig:
with open(input_file) as f:
return yaml.full_load(f)
def generate_repos_from_config(config: PreCommitConfig) -> list[PreCommitRepo]:
return [
{**repo.copy(), "hooks": [hook]}
for repo in config["repos"]
for hook in repo["hooks"]
]
def reorder_repos(repos: list[PreCommitRepo]):
with open("./order.txt", "w") as f:
for repo in repos:
for hook in repo["hooks"]:
f.write(hook["id"] + "\n")
input("Press any key once order.txt is sorted")
with open("./order.txt", "r") as f:
ids = f.read().splitlines()
return sorted(repos, key=lambda repo: ids.index(repo["hooks"][0]["id"]))
def merge_sibling_repos(repos: list[PreCommitRepo]) -> list[PreCommitRepo]:
while True:
repo_to_remove = None
for r, repo in enumerate(repos):
next_repo = repos[r + 1] if r + 1 < len(repos) else None
if not next_repo:
continue
if are_repos_equal(repo, next_repo):
repo["hooks"].extend(next_repo["hooks"])
repo_to_remove = next_repo
break
if repo_to_remove:
repos.remove(repo_to_remove)
else:
break
return repos
def write_config(repos: list[PreCommitRepo], output_file: str):
yaml.Dumper.ignore_aliases = lambda *args: True
with open(output_file, "w") as f:
yaml.dump({"repos": repos}, f, default_flow_style=False, sort_keys=False)
def main():
args = parse_args()
pre_commit_config = load_config(args["input"])
repos = generate_repos_from_config(pre_commit_config)
reorder_repos(repos)
merge_sibling_repos(repos)
write_config(repos, args["output"])
if __name__ == "__main__":
main()
from reorder_pre_commit_hooks import (
parse_args,
are_repos_equal,
load_config,
generate_repos_from_config,
reorder_repos,
merge_sibling_repos,
)
import sys
from unittest.mock import patch, mock_open
class TestParseArgs:
@staticmethod
def test_default_values():
sys.argv[1:] = []
args = parse_args()
assert args["dry"] is False
assert args["input"] == "./.pre-commit-config.yaml"
assert args["temp_order_file"] == "./hook-id-order.txt"
assert args["output"] == "./.pre-commit-config.yaml"
@staticmethod
def test_command_line_arguments():
sys.argv[1:] = [
"--dry",
"--input",
"input_file",
"--temp-order-file",
"temp_file",
"--output",
"output_file",
]
args = parse_args()
assert args["dry"] is True
assert args["input"] == "input_file"
assert args["temp_order_file"] == "temp_file"
assert args["output"] == "output_file"
class TestAreReposEqual:
@staticmethod
def test_true():
repo1 = {
"repo": "example_repo",
"rev": "123",
"hooks": [],
"additional_dependencies": [],
}
repo2 = {
"repo": "example_repo",
"rev": "123",
"hooks": [],
"additional_dependencies": [],
}
assert are_repos_equal(repo1, repo2)
@staticmethod
def test_false():
assert not are_repos_equal(
{
"repo": "example_repo",
"rev": "123",
"hooks": [],
"additional_dependencies": [],
},
{
"repo": "different_repo",
"rev": "456",
"hooks": [],
"additional_dependencies": [],
},
)
@staticmethod
def test_missing_rev():
assert are_repos_equal(
{"repo": "example_repo", "hooks": [], "additional_dependencies": []},
{"repo": "example_repo", "hooks": [], "additional_dependencies": []},
)
def test_load_config():
with patch("builtins.open", mock_open(read_data="key: value")) as mocked_open:
assert load_config("input_file") == {"key": "value"}
mocked_open.assert_called_once_with("input_file")
def test_generate_repos_from_config():
config = {
"repos": [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}, {"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
]
}
assert generate_repos_from_config(config) == [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
]
def test_reorder_repos():
repos = [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
]
with patch(
"builtins.open", mock_open(read_data="hook2\nhook1\nhook3")
) as mocked_open, patch("builtins.input", return_value="") as mock_input:
assert reorder_repos(repos) == [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
]
mocked_open.assert_any_call("./order.txt", "w")
mock_input.assert_called_once()
mocked_open.assert_called_with("./order.txt", "r")
def test_merge_sibling_repos():
repos = [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook4"}],
"additional_dependencies": [],
},
]
assert merge_sibling_repos(repos) == [
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook1"}, {"id": "hook2"}],
"additional_dependencies": [],
},
{
"repo": "repo2",
"rev": "456",
"hooks": [{"id": "hook3"}],
"additional_dependencies": [],
},
{
"repo": "repo1",
"rev": "123",
"hooks": [{"id": "hook4"}],
"additional_dependencies": [],
},
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment