Autohooks
Library for managing and writing git hooks in Python.
Looking for automatic formatting or linting, e.g., with black and pylint, while creating a git commit using a pure Python implementation? Welcome to autohooks!
Why?
Several outstanding libraries for managing and executing git hooks exist already.
To name a few: husky,
lint-staged,
precise-commits or
pre-commit.
However, they either need another interpreter besides python (like husky) or are
too ambiguous (like pre-commit). pre-commit is written in python but has support
hooks written in all kind of languages. Additionally, it maintains the dependencies by
itself and does not install them in the current environment.
Solution
autohooks is a pure python library that installs a minimal
executable git hook.
It allows the decision of how to maintain the hook dependencies
by supporting different modes.
Requirements
Python 3.7+ is required for autohooks.
Modes
Currently three modes for using autohooks are supported:
pythonpath
pipenv
poetry
These modes handle how autohooks, the plugins and their dependencies are loaded
during git hook execution.
If no mode is specified in the pyproject.toml
config file
and no mode is set during activation, autohooks
will use the pythonpath mode by default.
poetry
or pipenv
modes leverage the /usr/bin/env
command using the
--split-string
(-S
) option. If autohooks
detects that it is
running on an OS where /usr/bin/env
is yet to support split_strings
(notably ubuntu < 19.x), autohooks
will automatically change to an
internally chosen poetry_multiline
/pipenv_mutliline
mode. The
'multiline' modes should not be user-configured options; setting your
project to use poetry
or pipenv
allows team members the greatest
latitude to use an OS of their choice yet leverage the sane
/usr/bin/env --split-string
if possible. Though poetry_multiline
would generally work for all, it is very confusing sorcery.
(Multiline shebang explained)
Pythonpath Mode
In the pythonpath
mode, the user has to install autohooks, the desired
plugins and their dependencies into the PYTHONPATH
manually.
This can be achieved by running pip install --user autohooks ...
to put them
into the installation directory of the current user
or with pip install authooks ...
for a system wide installation.
Alternatively, a virtual environment
could be used separating the installation from the global and user wide
Python packages.
It is also possible to use [pipenv] for managing the virtual
environment but activating the environment has to be done manually.
Therefore it is even possible to run different versions of autohooks by
using the pythonpath
mode and switching to a virtual environment.
Pipenv Mode
In the pipenv
mode [pipenv] is used to run autohooks in a dedicated virtual
environment. Pipenv uses a lock file to install exact versions. Therefore the
installation is deterministic and reliable between different developer setups.
In contrast to the pythonpath
mode the activation of the virtual environment
provided by [pipenv] is done automatically in the background.
Poetry Mode
Like with the pipenv mode, it is possible to run autohooks in a
dedicated environment controlled by [poetry]. By using the poetry
mode the
virtual environment will be activated automatically in the background when
executing the autohooks based git commit hook.
Using the poetry
mode is highly recommended.
Installing autohooks
Four steps are necessary for installing autohooks:
- Choosing an autohooks mode
- Installing the autohooks python package into the current environment
- Configuring plugins to be run
- Activating the git hooks
1. Choosing an autohooks Mode
For its configuration, autohooks uses the pyproject.toml file specified in
PEP518.
Adding a [tool.autohooks] section allows to specify the desired autohooks mode
and to set python modules to be run as autohooks plugins.
The mode can be set by adding a mode =
line to the pyproject.toml file.
Current possible options are "pythonpath"
, "pipenv"
and "poetry"
(see
autohooks mode). If the mode setting is missing, the pythonpath
mode is used.
Example pyproject.toml:
[tool.autohooks]
mode = "pipenv"
2. Installing the autohooks Python Package into the Current Environment
Using [poetry] is highly recommended for installing the autohooks python package.
To install autohooks as a development dependency run
poetry add --dev autohooks
Alternatively, autohooks can be installed directly from GitHub by running
poetry add --dev git+https://github.com/greenbone/autohooks
3. Configuring Plugins to Be Run
To actually run an action on git hooks, autohooks plugins have to be
installed and configured, e.g., to install python linting via pylint run
poetry add --dev autohooks-plugin-pylint
Afterwards, the pylint plugin can be configured to run as a pre-commit git hook
by adding the autohooks-plugins-pylint python module name to the pre-commit
setting in the [tool.autohooks]
section in the pyproject.toml file.
Example pyproject.toml:
[tool.autohooks]
mode = "pipenv"
pre-commit = ["autohooks.plugins.pylint"]
4. Activating the Git Hooks
Because installing and activating git hooks automatically isn't reliable (with
using source distributions and different versions of pip) and even impossible
(with using wheels) the hooks need
to be activated manually once in each installation.
To activate the git hooks run
poetry run autohooks activate
Calling activate
also allows for overriding the mode defined in the
pyproject.toml settings for testing purposes.
Example:
autohooks activate --mode pipenv
Please keep in mind that autohooks will always issue a warning if the mode used
in the git hooks is different from the configured mode in the pyproject.toml
file.
The activation can always be verified by running autohooks check
.
Plugins
-
Python code formatting via black
-
Python code formatting via autopep8
-
Python code linting via pylint
-
Python import sorting via isort
Howto: Writing a Plugin
Plugins need to be available in the
Python import path. The
easiest way to achieve this is uploading a plugin to PyPI
and installing it via [pip] or [pipenv].
Alternatively, a plugin can also be put into a .autohooks directory in the root
directory of the git repository where the hooks should be executed.
An autohooks plugin is a Python module which provides a precommit function.
The function must accept arbitrary keywords because the keywords are likely to
change in future. Therefore using **kwargs is highly recommended.
Currently only a config keyword argument is passed to the precommit function.
Example:
def precommit(**kwargs):
config = kwargs.get('config')
The config can be used to receive settings from the pyproject.toml file, e.g.,
[tool.autohooks.plugins.foo]
bar = 2
can be received with
def precommit(**kwargs):
config = kwargs.get('config')
default_value = 1
setting = config
.get('tool', 'autohooks', 'plugins', 'foo')
.get_value('bar', default_value)
return 0
With autohooks it is possible to write all kinds of plugins. Most common are
plugins for linting and formatting.
Linting Plugin
Usually the standard call sequence for a linting plugin is the following:
- get list of staged files
- filter list of files for a specific file type
- stash unrelated changes
- apply checks on filtered list of files by calling some external tool
- raise exception if something did go wrong
- return 1 if check was not successful
- stage changes made by the tool
- unstash unrelated changes
- return 0
Example plugin:
import subprocess
from autohooks.api import ok, fail
from autohooks.api.git import get_staged_status, stash_unstaged_changes
from autohooks.api.path import match
DEFAULT_INCLUDE = ('*.ext')
def get_include(config)
if not config:
return DEFAULT_INCLUDE
config = config.get('tool', 'autohooks', 'plugins', 'foo')
return config.get_value('include', DEFAULT_INCLUDE)
def precommit(**kwargs):
config = kwargs.get('config')
include = get_include(config)
files = [f for f in get_staged_status() if match(f.path, include)]
if not files:
# not files to lint
return 0
with stash_unstaged_changes(files):
const failed = False
for file in files:
status = subprocess.call(['foolinter', str(file)])
if status:
fail('Could not validate {}'.format(str(file)))
failed = True
else:
ok('Validated {}'.format(str(file)))
return 1 if failed else 0
Formatting Plugin
Usually the standard call sequence for a formatting plugin is the following:
- get list of staged files
- filter list of files for a specific file type
- stash unrelated changes
- apply formatting on filtered list of files by calling some external tool
- raise exception if something did go wrong
- stage changes made by the tool
- unstash unrelated changes
- return 0
Example plugin:
import subprocess
from autohooks.api import ok, error
from autohooks.api.git import (
get_staged_status,
stage_files_from_status_list,
stash_unstaged_changes,
)
from autohooks.api.path import match
DEFAULT_INCLUDE = ('*.ext')
def get_include(config)
if not config:
return DEFAULT_INCLUDE
config = config.get('tool', 'autohooks', 'plugins', 'bar')
return config.get_value('include', DEFAULT_INCLUDE)
def precommit(**kwargs):
config = kwargs.get('config')
include = get_include(config)
files = [f for f in get_staged_status() if match(f.path, include)]
if not files:
# not files to format
return 0
with stash_unstaged_changes(files):
for file in files:
# run formatter and raise exception if it fails
subprocess.run(['barformatter', str(file)], check=True)
ok('Formatted {}'.format(str(file)))
return 0