Create your own pre-commit hook

jalvaradosegura

Jorge Alvarado

Posted on February 23, 2022

Create your own pre-commit hook

In this tutorial we are going to create a very basic git hook, using the awesome package pre-commit, created by the one and only Anthony Sottile šŸ‘šŸ‘.

The hook will be very basic. It won't have unit-tests nor will be very functional for a real project. Although it will show you the first steps you need to take in order to create something incredible.

I recommend you using a virtual environment to follow this tutorial.

Tutorial Index

  1. Create the structure of the project
  2. Create the functionality
  3. Turn it into a python package
  4. Turn it into a hook
  5. Test it

1. Create the structure of the project

Create the following project structure:

.
ā”œā”€ā”€ test-the-hook-in-this-folder
ā””ā”€ā”€ the-hook
    ā”œā”€ā”€ print_arguments
    ā”‚Ā Ā  ā”œā”€ā”€ __init__.py
    ā”‚Ā Ā  ā””ā”€ā”€ main.py
    ā”œā”€ā”€ setup.cfg
    ā””ā”€ā”€ setup.py
Enter fullscreen mode Exit fullscreen mode
  • test-the-hook-in-this-folder: Folder in which we will test our hook by the end of the tutorial (we won't use it until step 5).
  • the-hook: Folder that contains all the files required for the package.
  • print_arguments: This is our python package (and also a spoiler of what our hook will do).
  • __init__.py: Turn the folder that contains it into a package.
  • main.py: Here is where the logic of the hook will be.
  • setup.py: Necessary for building our package.
  • setup.cfg: Describe our package.

2. Create the functionality

We will work in the the-hook folder for steps 2 to 4.

For creating the functionality, we only need to edit the main.py file:

# print_arguments/main.py
import argparse


def print_arguments(arguments: list[str]):
    for argument in arguments:
        print(argument)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("filenames", nargs="*")
    args = parser.parse_args()

    print_arguments(args.filenames)


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Yes, our hook will print the arguments we pass to it. We can test its functionality:

python print_arguments/main.py arg1 arg2 arg3
Enter fullscreen mode Exit fullscreen mode

Output:

arg1
arg2
arg3
Enter fullscreen mode Exit fullscreen mode

3. Turn it into a python package

To do this, we need to edit our 2 setup files.

setup.py:

# setup.py
from setuptools import setup

setup()
Enter fullscreen mode Exit fullscreen mode

setup.cfg:

# setup.cfg
[metadata]
name = print-arguments
description = print the arguments you pass
version = 0.1.0
author = Jorge Alvarado
author_email = alvaradosegurajorge@gmail.com
license = MIT
url = https://jorgealvarado.me

[options]
packages = find:

[options.entry_points]
console_scripts =
    print-arguments = print_arguments.main:main
Enter fullscreen mode Exit fullscreen mode

Important points:

  • packages = find:: It help us find our print_arguments package when packaging.
  • console_scripts: This is our entry point, with this we kind of expose our function to the world. The name of our single entry point, in this case print-arguments, will be used by pre-commit.

You can choose other values for the rest of the points.

Install the package with:

pip install .
Enter fullscreen mode Exit fullscreen mode

Make sure you are in the-hook folder when running that command.

4. Turn it into a hook

For our hook to work we need our project to be a git repository. So let's do that by running the following commands (make sure you are in the the-hook folder):

Initialize the git repository:

git init
Enter fullscreen mode Exit fullscreen mode

Add every file and commit them:

git add .; git commit -m "Create the package"
Enter fullscreen mode Exit fullscreen mode

Now the hook part. First let's install pre-commit:

pip install pre-commit
Enter fullscreen mode Exit fullscreen mode

Create a .pre-commit-hooks.yaml file inside the the-hook folder and edit it:

# .pre-commit-hooks.yaml
- id: some-id
  name: some-name
  description: some description
  entry: print-arguments
  language: python
Enter fullscreen mode Exit fullscreen mode
  • id: The id of our hook. We will need it for testing and in the future if someone wants to use our hook, the id will be required.
  • name: The name of the hook, it is what is shown during hook execution.
  • description (optional): Description of the hook.
  • entry: The entry point, the executable to run. It has to match with the entry point name defined inside our setup.cfg.
  • language: The language of the hook, it tells pre-commit how to install the hook.

šŸ‘€ See a complete list of the options.

To complete this step, we need to do a commit of our hook stuff:

git add .; git commit -m "Create a hook"
Enter fullscreen mode Exit fullscreen mode

5. Test it

Time to use the test-the-hook-in-this-folder folder. Change directory to that folder, once there, let's create a very little git project:

Create some files

touch a.py b.py
Enter fullscreen mode Exit fullscreen mode

Initialize the git repository:

git init
Enter fullscreen mode Exit fullscreen mode

Commit the files:

git add .; git commit -m "Create the project"
Enter fullscreen mode Exit fullscreen mode

Finally, let's test our hook:

pre-commit try-repo ../the-hook some-id --verbose --all-files
Enter fullscreen mode Exit fullscreen mode

Output:

some-name................................................................Passed
- hook id: some-id
- duration: 0.08s

a.py
b.py
Enter fullscreen mode Exit fullscreen mode

Notice:

  • We got some-name in the output, because that's what we defined in our .pre-commit-hooks.yaml file.
  • We used some-id (defined in the .pre-commit-hooks.yaml file as well) in the command to refer to our hook (we could have multiple hooks).
  • a.py and b.py were printed. That's because we used the flag --all-files in our command (If we didn't, the hook execution would have been skipped, because there are no new/edited files for git).

Remember that you need to be in the test-the-hook-in-this-folder folder for the command just used to work.

If we now add a new file to our test folder:

touch c.py
Enter fullscreen mode Exit fullscreen mode

We track it with git:

git add c.py
Enter fullscreen mode Exit fullscreen mode

And then we run the command without the --all-files flag:

pre-commit try-repo ../the-hook some-id --verbose
Enter fullscreen mode Exit fullscreen mode

We get:

some-name................................................................Passed
- hook id: some-id
- duration: 0.07s

c.py
Enter fullscreen mode Exit fullscreen mode

As you can see, only c.py was printed this time, because is the only new/edited file for git, so our hook was run only against that file. This is the behavior you will normally want.


Now you know how to create a pre-commit hook šŸŖ! Go and make cool things with this knowledge šŸ˜.

Let me know if you create a hook or something. This is one I created a few weeks ago.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
jalvaradosegura
Jorge Alvarado

Posted on February 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Create your own pre-commit hook
tutorial Create your own pre-commit hook

February 23, 2022