From 727fe1aa4cb25698174aa8f76bddb0649b4f95cb Mon Sep 17 00:00:00 2001 From: Nicolas Thumann Date: Mon, 23 Feb 2026 17:07:55 +0100 Subject: [PATCH 1/4] Add: Support for uv --- autohooks/cli/__init__.py | 3 ++- autohooks/config.py | 4 +++- autohooks/hooks.py | 7 +++++++ autohooks/settings.py | 10 ++++++++-- autohooks/template.py | 12 ++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/autohooks/cli/__init__.py b/autohooks/cli/__init__.py index 78e3e040..6a633e50 100644 --- a/autohooks/cli/__init__.py +++ b/autohooks/cli/__init__.py @@ -50,9 +50,10 @@ def main(): str(Mode.PYTHONPATH), str(Mode.PIPENV), str(Mode.POETRY), + str(Mode.UV) ], help="Mode for loading autohooks during hook execution. Either load " - "autohooks from the PYTHON_PATH, via pipenv or via poetry.", + "autohooks from the PYTHON_PATH, via pipenv, via poetry or via uv.", ) activate_parser.set_defaults(func=install_hooks) diff --git a/autohooks/config.py b/autohooks/config.py index a3b72ba8..b1732c62 100644 --- a/autohooks/config.py +++ b/autohooks/config.py @@ -83,10 +83,12 @@ def _gather_mode(mode_string: Optional[str]) -> Mode: Gather the mode from a mode string """ mode = Mode.from_string(mode_string) - is_virtual_env = mode == Mode.PIPENV or mode == Mode.POETRY + is_virtual_env = mode == Mode.PIPENV or mode == Mode.POETRY or mode == Mode.UV if is_virtual_env and not is_split_env(): if mode == Mode.POETRY: mode = Mode.POETRY_MULTILINE + elif mode == Mode.UV: + mode = Mode.UV_MULTILINE else: mode = Mode.PIPENV_MULTILINE return mode diff --git a/autohooks/hooks.py b/autohooks/hooks.py index f2a1ef61..4f387602 100644 --- a/autohooks/hooks.py +++ b/autohooks/hooks.py @@ -14,6 +14,8 @@ POETRY_MULTILINE_SHEBANG, POETRY_SHEBANG, PYTHON3_SHEBANG, + UV_SHEBANG, + UV_MULTILINE_SHEBANG, TEMPLATE_VERSION, PreCommitTemplate, ) @@ -65,6 +67,8 @@ def read_mode(self) -> Mode: return Mode.POETRY if shebang == PIPENV_SHEBANG: return Mode.PIPENV + if shebang == UV_SHEBANG: + return Mode.UV shebang = f"{lines[0][2:]}\n" shebang += "\n".join(lines[1:5]) @@ -74,6 +78,9 @@ def read_mode(self) -> Mode: if shebang == PIPENV_MULTILINE_SHEBANG: return Mode.PIPENV_MULTILINE + if shebang == UV_MULTILINE_SHEBANG: + return Mode.UV_MULTILINE + return Mode.UNKNOWN def read_version(self) -> int: diff --git a/autohooks/settings.py b/autohooks/settings.py index 0777f841..bbc8f5f3 100644 --- a/autohooks/settings.py +++ b/autohooks/settings.py @@ -15,8 +15,10 @@ class Mode(Enum): PIPENV = 1 PYTHONPATH = 2 POETRY = 3 - PIPENV_MULTILINE = 4 - POETRY_MULTILINE = 5 + UV = 4 + PIPENV_MULTILINE = 5 + POETRY_MULTILINE = 6 + UV_MULTILINE = 7 UNDEFINED = -1 UNKNOWN = -2 @@ -29,6 +31,10 @@ def get_effective_mode(self): return Mode.POETRY if self.value == Mode.POETRY_MULTILINE.value: return Mode.POETRY_MULTILINE + if self.value == Mode.UV.value: + return Mode.UV + if self.value == Mode.UV_MULTILINE.value: + return Mode.UV_MULTILINE return Mode.PYTHONPATH @staticmethod diff --git a/autohooks/template.py b/autohooks/template.py index 765c3e6f..9453ad47 100644 --- a/autohooks/template.py +++ b/autohooks/template.py @@ -13,6 +13,7 @@ PYTHON3_SHEBANG = "/usr/bin/env python3" PIPENV_SHEBANG = "/usr/bin/env -S pipenv run python3" POETRY_SHEBANG = "/usr/bin/env -S poetry run python" +UV_SHEBANG = "/usr/bin/env -S uv run python" # For OS's that don't support '/usr/bin/env -S'. PIPENV_MULTILINE_SHEBANG = ( "/bin/sh\n" @@ -28,6 +29,13 @@ 'exit "$?"\n' "'''" ) +UV_MULTILINE_SHEBANG = ( + "/bin/sh\n" + "\"true\" ''':'\n" + 'uv run python "$0" "$@"\n' + 'exit "$?"\n' + "'''" +) TEMPLATE_VERSION = 1 @@ -55,10 +63,14 @@ def render(self, *, mode: Mode) -> str: params["SHEBANG"] = PIPENV_SHEBANG elif mode == Mode.POETRY: params["SHEBANG"] = POETRY_SHEBANG + elif mode == Mode.UV: + params["SHEBANG"] = UV_SHEBANG elif mode == Mode.PIPENV_MULTILINE: params["SHEBANG"] = PIPENV_MULTILINE_SHEBANG elif mode == Mode.POETRY_MULTILINE: params["SHEBANG"] = POETRY_MULTILINE_SHEBANG + elif mode == Mode.UV_MULTILINE: + params["SHEBANG"] = UV_MULTILINE_SHEBANG else: params["SHEBANG"] = PYTHON3_SHEBANG From 1345f32e254fe733673cb8808c48e011f5736732 Mon Sep 17 00:00:00 2001 From: Nicolas Thumann Date: Mon, 23 Feb 2026 17:08:09 +0100 Subject: [PATCH 2/4] Add: Unit tests for uv mode --- tests/test_config.py | 20 ++++++++++++++++++++ tests/test_hooks.py | 26 ++++++++++++++++++++++++++ tests/test_settings.py | 16 ++++++++++++++++ tests/test_template.py | 22 ++++++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index a8bdc538..4b0919f4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -129,6 +129,26 @@ def test_get_mode_poetry_multiline(self): self.assertEqual(len(config.get_pre_commit_script_names()), 0) + def test_get_mode_uv_multiline(self): + config = AutohooksConfig.from_dict( + {"tool": {"autohooks": {"mode": "uv_multiline"}}} + ) + + self.assertTrue(config.has_autohooks_config()) + self.assertEqual(config.get_mode(), Mode.UV_MULTILINE) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + + def test_get_mode_uv(self): + config = AutohooksConfig.from_dict( + {"tool": {"autohooks": {"mode": "uv"}}} + ) + + self.assertTrue(config.has_autohooks_config()) + self.assertEqual(config.get_mode(), Mode.UV) + + self.assertEqual(len(config.get_pre_commit_script_names()), 0) + def test_get_mode_pythonpath(self): config = AutohooksConfig.from_dict( {"tool": {"autohooks": {"mode": "pythonpath"}}} diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 45174905..9648063d 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -19,6 +19,8 @@ POETRY_SHEBANG, PYTHON3_SHEBANG, TEMPLATE_VERSION, + UV_MULTILINE_SHEBANG, + UV_SHEBANG, PreCommitTemplate, ) from autohooks.utils import exec_git @@ -161,6 +163,12 @@ def test_poetry_mode(self): self.assertEqual(pre_commit_hook.read_mode(), Mode.POETRY) + def test_uv_mode(self): + path = FakeReadPath(f"#!{UV_SHEBANG}") + pre_commit_hook = PreCommitHook(path) + + self.assertEqual(pre_commit_hook.read_mode(), Mode.UV) + def test_poetry_mode_with_python3(self): path = FakeReadPath(f"#!{POETRY_SHEBANG}3") pre_commit_hook = PreCommitHook(path) @@ -179,6 +187,12 @@ def test_poetry_multiline_mode(self): self.assertEqual(pre_commit_hook.read_mode(), Mode.POETRY_MULTILINE) + def test_uv_multiline_mode(self): + path = FakeReadPath(f"#!{UV_MULTILINE_SHEBANG}") + pre_commit_hook = PreCommitHook(path) + + self.assertEqual(pre_commit_hook.read_mode(), Mode.UV_MULTILINE) + def test_pythonpath_mode(self): path = FakeReadPath(f"#!{PYTHON3_SHEBANG}") pre_commit_hook = PreCommitHook(path) @@ -211,6 +225,18 @@ def test_poetry_mode(self): text = args[0] self.assertRegex(text, f"^#!{POETRY_SHEBANG} *") + def test_uv_mode(self): + write_path = Mock() + pre_commit_hook = PreCommitHook(write_path) + pre_commit_hook.write(mode=Mode.UV) + + write_path.chmod.assert_called_with(0o775) + self.assertTrue(write_path.write_text.called) + + args, _kwargs = write_path.write_text.call_args + text = args[0] + self.assertRegex(text, f"^#!{UV_SHEBANG} *") + def test_pythonpath_mode(self): write_path = Mock() pre_commit_hook = PreCommitHook(write_path) diff --git a/tests/test_settings.py b/tests/test_settings.py index ff931881..fcc8a287 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -22,6 +22,10 @@ def test_get_effective_mode(self): self.assertEqual( Mode.POETRY_MULTILINE.get_effective_mode(), Mode.POETRY_MULTILINE ) + self.assertEqual(Mode.UV.get_effective_mode(), Mode.UV) + self.assertEqual( + Mode.UV_MULTILINE.get_effective_mode(), Mode.UV_MULTILINE + ) self.assertEqual(Mode.UNDEFINED.get_effective_mode(), Mode.PYTHONPATH) self.assertEqual(Mode.UNKNOWN.get_effective_mode(), Mode.PYTHONPATH) @@ -53,6 +57,18 @@ def test_get_poetry_multiline_mode_from_string(self): Mode.from_string("POETRY_MULTILINE"), Mode.POETRY_MULTILINE ) + def test_get_uv_mode_from_string(self): + self.assertEqual(Mode.from_string("uv"), Mode.UV) + self.assertEqual(Mode.from_string("UV"), Mode.UV) + + def test_get_uv_multiline_mode_from_string(self): + self.assertEqual( + Mode.from_string("uv_multiline"), Mode.UV_MULTILINE + ) + self.assertEqual( + Mode.from_string("UV_MULTILINE"), Mode.UV_MULTILINE + ) + def test_get_invalid_mode_from_string(self): self.assertEqual(Mode.from_string("foo"), Mode.UNKNOWN) self.assertEqual(Mode.from_string(None), Mode.UNDEFINED) diff --git a/tests/test_template.py b/tests/test_template.py index 9a69f453..6083c5a1 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -60,6 +60,14 @@ def test_should_render_mode_poetry(self): "/usr/bin/env -S poetry run python", ) + def test_should_render_mode_uv(self): + path = FakeTemplatePath("$SHEBANG") + template = PreCommitTemplate(path) + self.assertEqual( + template.render(mode=Mode.UV), + "/usr/bin/env -S uv run python", + ) + def test_should_render_mode_pipenv_multiline(self): path = FakeTemplatePath("$SHEBANG") template = PreCommitTemplate(path) @@ -88,6 +96,20 @@ def test_should_render_mode_poetry_multiline(self): ), ) + def test_should_render_mode_uv_multiline(self): + path = FakeTemplatePath("$SHEBANG") + template = PreCommitTemplate(path) + self.assertEqual( + template.render(mode=Mode.UV_MULTILINE), + ( + "/bin/sh\n" + "\"true\" ''':'\n" + 'uv run python "$0" "$@"\n' + 'exit "$?"\n' + "'''" + ), + ) + def test_should_render_mode_unknown(self): path = FakeTemplatePath("$SHEBANG") template = PreCommitTemplate(path) From e9d53451f51bd65b6d195b847811b1be4b023f89 Mon Sep 17 00:00:00 2001 From: Nicolas Thumann Date: Mon, 23 Feb 2026 17:08:15 +0100 Subject: [PATCH 3/4] Change: Update documentation --- README.md | 2 ++ docs/configuration.md | 21 ++++++++++++++++++++- docs/installation.md | 23 +++++++++++++++++++++++ docs/modes.md | 18 ++++++++++++++---- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6d6bd11f..36d6f262 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ autohooks: * `pythonpath` for dependency management via [pip] * `poetry` for dependency management via [poetry] (recommended) * `pipenv` for dependency management via [pipenv] +* `uv` for dependency management via [uv] These modes handle how autohooks, the plugins and their dependencies are loaded during git hook execution. @@ -165,3 +166,4 @@ Licensed under the [GNU General Public License v3.0 or later](LICENSE). [poetry]: https://python-poetry.org/ [pylint]: https://pylint.readthedocs.io/en/latest/ [ruff]: https://docs.astral.sh/ruff/ +[uv]: https://docs.astral.sh/uv/ \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index aa53d8dd..ad4a07e3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,7 @@ and to set python modules to be run as [autohooks plugins](./plugins). ## Mode Configuration The mode can be set by adding a `mode =` line to the *pyproject.toml* file. -Current possible options are `"pythonpath"`, `"pipenv"` and `"poetry"` (see +Current possible options are `"pythonpath"`, `"pipenv"`, `"poetry"` and `"uv"`(see [autohooks mode](./modes)). If the mode setting is missing, the `pythonpath` mode is used. @@ -40,6 +40,13 @@ mode = "pythonpath" ``` ```` +````{group-tab} uv +```toml +[tool.autohooks] +mode = "uv" +``` +```` + ````` Calling `autohooks activate` also allows for overriding the [mode](./modes) @@ -66,6 +73,12 @@ autohooks activate --mode pythonpath --force ``` ```` +````{group-tab} uv +```bash +autohooks activate --mode uv --force +``` +```` + ````` Please keep in mind that autohooks will always issue a warning if the mode used @@ -110,4 +123,10 @@ autohooks plugins add autohooks.plugins.pylint ``` ```` +````{group-tab} uv +```bash +uv run autohooks plugins add autohooks.plugins.pylint +``` +```` + ````` diff --git a/docs/installation.md b/docs/installation.md index b9ed7be4..b2775190 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -11,6 +11,7 @@ For fulfilling these steps different tooling can be used. Most prominent and cur * [poetry] * [pipenv] * [pip] +* [uv] ```{note} Using [poetry] is recommended for installing and maintaining autohooks. @@ -56,6 +57,14 @@ To install autohooks globally for the current user run python3 -m pip install --user autohooks ``` ````` + +`````{group-tab} uv +To install autohooks as a development dependency run + +```{code-block} +uv add --dev autohooks +``` +````` `````` ## 2. Activating the Git Hook @@ -82,6 +91,12 @@ pipenv run autohooks activate --mode pipenv autohooks activate --mode pythonpath ``` ```` + +````{group-tab} uv +```bash +uv run autohooks activate --mode uv +``` +```` ````` ## 3. Installing and Configuring the Plugins to Be Run @@ -111,7 +126,15 @@ python3 -m pip install --user autohooks-plugin-pylint autohooks plugins add autohooks.plugins.pylint ``` ```` + +````{group-tab} uv +```bash +uv add --dev autohooks-plugin-pylint +uv run autohooks plugins add autohooks.plugins.pylint +``` +```` ````` [pipenv]: https://pipenv.readthedocs.io/en/latest/ [poetry]: https://python-poetry.org/ [pip]: https://pip.pypa.io/en/stable/ +[uv]: https://docs.astral.sh/uv/ \ No newline at end of file diff --git a/docs/modes.md b/docs/modes.md index 729aad49..524ca5dd 100644 --- a/docs/modes.md +++ b/docs/modes.md @@ -5,6 +5,7 @@ Currently three modes for using autohooks are supported: * `pythonpath` * `poetry` * `pipenv` +* `uv` These modes handle how autohooks, the plugins and their dependencies are loaded during git hook execution. @@ -14,13 +15,13 @@ and no mode is set during [activation](./installation.md), autohooks will use the [pythonpath mode](#pythonpath-mode) by default. ```{note} -`poetry` or `pipenv` modes leverage the `/usr/bin/env` command using the +`poetry`, `pipenv` or `uv` 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 +internally chosen `poetry_multiline`/`pipenv_mutliline`/`uv_multiline` mode. The 'multiline' modes *should not* be user-configured options; setting your -project to use `poetry` or `pipenv`allows team members the greatest +project to use `poetry`, `pipenv` or `uv` 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. @@ -44,7 +45,7 @@ this mode with a virtual environment is that [activating the environment](https: has to be done manually. To benefit from the advantages of a virtual environment a much better choice is -to use [poetry] or [pipenv] for managing the virtual environment automatically. +to use [poetry], [pipenv] or [uv] for managing the virtual environment automatically. ## Poetry Mode @@ -67,5 +68,14 @@ 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. +## uv Mode + +With the `uv` mode it is possible to run autohooks in a +dedicated environment controlled by [uv]. By using the `uv` mode the +virtual environment will be activated automatically in the background when +executing the autohooks based git commit hook. All dependencies are managed +by uv using the `pyproject.toml` and `uv.lock` files. + [pipenv]: https://pipenv.readthedocs.io/en/latest/ [poetry]: https://python-poetry.org/ +[uv]: https://docs.astral.sh/uv/ \ No newline at end of file From 9504499feadbbd99f82faf08cc108d119065e118 Mon Sep 17 00:00:00 2001 From: Nicolas Thumann Date: Mon, 23 Feb 2026 17:18:17 +0100 Subject: [PATCH 4/4] Change: Make ruff & black happy --- autohooks/cli/__init__.py | 2 +- autohooks/config.py | 4 +++- autohooks/hooks.py | 4 ++-- tests/test_settings.py | 8 ++------ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/autohooks/cli/__init__.py b/autohooks/cli/__init__.py index 6a633e50..2fe3938f 100644 --- a/autohooks/cli/__init__.py +++ b/autohooks/cli/__init__.py @@ -50,7 +50,7 @@ def main(): str(Mode.PYTHONPATH), str(Mode.PIPENV), str(Mode.POETRY), - str(Mode.UV) + str(Mode.UV), ], help="Mode for loading autohooks during hook execution. Either load " "autohooks from the PYTHON_PATH, via pipenv, via poetry or via uv.", diff --git a/autohooks/config.py b/autohooks/config.py index b1732c62..6a9c9e2f 100644 --- a/autohooks/config.py +++ b/autohooks/config.py @@ -83,7 +83,9 @@ def _gather_mode(mode_string: Optional[str]) -> Mode: Gather the mode from a mode string """ mode = Mode.from_string(mode_string) - is_virtual_env = mode == Mode.PIPENV or mode == Mode.POETRY or mode == Mode.UV + is_virtual_env = ( + mode == Mode.PIPENV or mode == Mode.POETRY or mode == Mode.UV + ) if is_virtual_env and not is_split_env(): if mode == Mode.POETRY: mode = Mode.POETRY_MULTILINE diff --git a/autohooks/hooks.py b/autohooks/hooks.py index 4f387602..03a9da42 100644 --- a/autohooks/hooks.py +++ b/autohooks/hooks.py @@ -14,9 +14,9 @@ POETRY_MULTILINE_SHEBANG, POETRY_SHEBANG, PYTHON3_SHEBANG, - UV_SHEBANG, - UV_MULTILINE_SHEBANG, TEMPLATE_VERSION, + UV_MULTILINE_SHEBANG, + UV_SHEBANG, PreCommitTemplate, ) from autohooks.utils import get_git_hook_directory_path diff --git a/tests/test_settings.py b/tests/test_settings.py index fcc8a287..575efe65 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -62,12 +62,8 @@ def test_get_uv_mode_from_string(self): self.assertEqual(Mode.from_string("UV"), Mode.UV) def test_get_uv_multiline_mode_from_string(self): - self.assertEqual( - Mode.from_string("uv_multiline"), Mode.UV_MULTILINE - ) - self.assertEqual( - Mode.from_string("UV_MULTILINE"), Mode.UV_MULTILINE - ) + self.assertEqual(Mode.from_string("uv_multiline"), Mode.UV_MULTILINE) + self.assertEqual(Mode.from_string("UV_MULTILINE"), Mode.UV_MULTILINE) def test_get_invalid_mode_from_string(self): self.assertEqual(Mode.from_string("foo"), Mode.UNKNOWN)