Repository: numpy/numtype
Pull request #394: 🧑‍💻 add `lefthook` for pre-commit formatting and linting
Author: login: guan404ming
Labels: name: topic: automation
name: topic: developer experience
Created: 2025-03-24T08:00:14Z | Merged: 2025-03-24T15:01:01Z

Close #389

Linked issue #389: Investigate Lefthook
It appears to be a superior (and unaffiliated) alternative to pre-commit https://lefthook.dev/intro.html

ref: #388

Diff:
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 00000000..0214dfe7
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,26 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
+      run: uv run --no-group=orjson mdformat {staged_files}
+      stage-fixed: true
+
+post-checkout:
+  commands:
+    dependencies:
+      glob: uv.lock
+      run: uv sync
+
+post-merge:
+  files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD"
+  commands:
+    dependencies:
+      glob: uv.lock
+      run: uv sync
diff --git a/pyproject.toml b/pyproject.toml
index 68cb84de..e328f7dd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,8 @@ dependencies = []
 [dependency-groups]
 dev = [
     "basedpyright>=1.28.3",
+    "lefthook>=1.11.3",
+    "mdformat>=0.7.22",
     "mypy>=1.15.0",
     "pytest>=8.3.5",
     "ruff>=0.11.2",
diff --git a/uv.lock b/uv.lock
index 9b099767..1cb5cb90 100644
--- a/uv.lock
+++ b/uv.lock
@@ -41,6 +41,49 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
 ]
 
+[[package]]
+name = "lefthook"
+version = "1.11.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/e268be2d6792ca0ffc974af162ff7fa4a83b308c8fac1e8a5abb1ebd1e36/lefthook-1.11.3.tar.gz", hash = "sha256:5520fc669072c81d3ab103608d1bea39aa901f81eba08e069485599de5362415", size = 46559864 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d9/c1/74590fa9cb5d799dde99552341af5a3c6df4b15026edd4487c2f1195f5ee/lefthook-1.11.3-py3-none-any.whl", hash = "sha256:ab93b75ba936f19a194b1998725e7e4c59c8469da42c496d1a47231e72e96265", size = 46782823 },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "mdformat"
+version = "0.7.22"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/eb/b5cbf2484411af039a3d4aeb53a5160fae25dd8c84af6a4243bc2f3fedb3/mdformat-0.7.22.tar.gz", hash = "sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea", size = 34610 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f2/6f/94a7344f6d634fe3563bea8b33bccedee37f2726f7807e9a58440dc91627/mdformat-0.7.22-py3-none-any.whl", hash = "sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5", size = 34447 },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
 [[package]]
 name = "mypy"
 version = "1.15.0"
@@ -179,6 +222,8 @@ numpy = [
 [package.dev-dependencies]
 dev = [
     { name = "basedpyright" },
+    { name = "lefthook" },
+    { name = "mdformat" },
     { name = "mypy" },
     { name = "pytest" },
     { name = "ruff" },
@@ -201,6 +246,8 @@ provides-extras = ["numpy"]
 [package.metadata.requires-dev]
 dev = [
     { name = "basedpyright", specifier = ">=1.28.3" },
+    { name = "lefthook", specifier = ">=1.11.3" },
+    { name = "mdformat", specifier = ">=0.7.22" },
     { name = "mypy", specifier = ">=1.15.0" },
     { name = "pytest", specifier = ">=8.3.5" },
     { name = "ruff", specifier = ">=0.11.2" },


Reviews:
user: login: jorenham
body: We should probably add some excludes for `.venv` and stuff

https://lefthook.dev/examples/filters.html
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:41:02Z
user: login: guan404ming
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:57:59Z
user: login: guan404ming
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:58:53Z
user: login: guan404ming
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:59:17Z
user: login: jorenham
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T14:26:56Z
user: login: jorenham
body: I stumbled upon https://github.com/disrupted/things-cloud-api/blob/main/.lefthook.yaml; those `post-checkout` and `post-merge`  look pretty handy to me. Should we also include those here?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T14:29:06Z
user: login: jorenham
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T14:55:30Z

Review comments:
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
path: lefthook.yml
user: login: jorenham
body: will this also format any `.md` in the `.venv` packages?
created_at: 2025-03-24T13:36:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
path: lefthook.yml
user: login: jorenham
body: does this include `.venv` ?
created_at: 2025-03-24T13:37:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
path: lefthook.yml
user: login: jorenham
body: is this needed here? `ruff check` doesn't modify any files
created_at: 2025-03-24T13:39:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
+      run: uv run mdformat {staged_files}
path: lefthook.yml
user: login: jorenham
body: ```suggestion
      run: uv run --no-group=orjson mdformat {staged_files}
```
created_at: 2025-03-24T13:39:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
path: lefthook.yml
user: login: guan404ming
body: I think it's not likely to format anything in the `.venv` since I use {staged_files} and `.venv` is listed in `.gitignore` thus it won't be able to be staged which means it won't never be formatted
created_at: 2025-03-24T13:57:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
path: lefthook.yml
user: login: guan404ming
body: https://github.com/numpy/numtype/pull/394#discussion_r2010240046
created_at: 2025-03-24T13:58:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
path: lefthook.yml
user: login: guan404ming
body: Agreed with you, I would remove it~
created_at: 2025-03-24T13:59:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    lint:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff check {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
path: lefthook.yml
user: login: jorenham
body: ah of course! sorry about the noise then
created_at: 2025-03-24T14:26:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,26 @@
+# lefthook.yml
+
+pre-commit:
+  parallel: true
+  commands:
+    format:
+      glob: "*.{py,pyi}"
+      run: uv run --no-group=orjson ruff format {staged_files}
+      stage-fixed: true
+    markdown-lint:
+      glob: "*.md"
+      run: uv run --no-group=orjson mdformat {staged_files}
+      stage-fixed: true
+
+post-checkout:
+commands:
+  dependencies:
+    glob: uv.lock
+    run: uv sync
path: lefthook.yml
user: login: jorenham
body: ```suggestion
  commands:
    dependencies:
      glob: uv.lock
      run: uv sync
```
created_at: 2025-03-24T14:55:30Z
author_association: MEMBER

PR comments:
user: login: guan404ming
created_at: 2025-03-24T10:22:01Z
body: Currently I only implement the lint, format and markdownlint since type check take a lots time~
author_association: MEMBER
user: login: jorenham
created_at: 2025-03-24T13:41:51Z
body: > Currently I only implement the lint, format and markdownlint since type check take a lots time~

I agree. Hopefully red-knot will change that in the future.
author_association: MEMBER
user: login: guan404ming
created_at: 2025-03-24T14:09:42Z
body: ref: https://github.com/numpy/numtype/pull/394#discussion_r2010240046

I think that to add the filter is not really necessary since we have already listed all files that we don't want it to be added into git in `.gitignore`. Thus it may not be staged and won't be formatted by `lefthook`. 

But I personally +0 point about this topic, do you have any idea about this?
author_association: MEMBER
user: login: jorenham
created_at: 2025-03-24T14:30:54Z
body: > ref: [#394 (comment)](https://github.com/numpy/numtype/pull/394#discussion_r2010240046)
> 
> I think that to add the filter is not really necessary since we have already listed all files that we don't want it to be added into git in `.gitignore`. Thus it may not be staged and won't be formatted by `lefthook`.
> 
> But I personally +0 point about this topic, do you have any idea about this?

What you said about it not being staged makes sense, so let's not worry about it.

And in case we notice that our `.venv` contents changed regardless, then I suppose we know what to do now 🤷🏻 
author_association: MEMBER
user: login: guan404ming
created_at: 2025-03-24T14:53:22Z
body: > I stumbled upon https://github.com/disrupted/things-cloud-api/blob/main/.lefthook.yaml; those `post-checkout` and `post-merge` look pretty handy to me. Should we also include those here?

Those are handy to me too. I have added it.
author_association: MEMBER
user: login: jorenham
created_at: 2025-03-24T15:01:39Z
body: Thanks Guan-Ming 👌🏻 
author_association: MEMBER