You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@DavisVaughan your observations are all correct IMO. Python must not necessarily be available, you can also install pre-commit with brew for example, then it will manage the python installation I think. Note that pre-commit always clones a repo using a tag or a hash from rev: field in the .pre-commit-config.yaml, which is then installed (with all its dependencies).
Unifying approach
Hence, you could also place the .pre-commit-config.yaml file in the posit-dev/air repo directly. Then, if you push new tags to this repo (which I guess you do anyways if you release a new version), pre-commit autoupdate will recognise them and pre-commit autoupdate will pick it up. Note pre-commit autoupdate --bleeding-edge or so uses always the latest commit hash from the default branch, but few people seem to use that.
Splitting approach
You could also follow ruff and create a repo like posit-dev/air-pre-commit. If you don't publish the crates (which I assume you don't currently according to #98), this approach is not available to you I think, since your air-pre-commit repo can't list air as a dependency that can be resolved and installed by a package manager for rust. Here's how you'd make hook updates available to your users:
bump the dependency of the posit-dev/air-pre-commit on air.
push a new tag corresponding to the new version to posit-dev/air-pre-commit to ensure that pre-commit autoupdate fetches the latest version of the hook.
There are many examples where the pre-commit hooks configs are directly in the root of the actual project. I'd say considerations pro and con of unifying vs splitting:
easier update process if unifying (as described above).
git clone a lightweight git repo and then installing air as a dependency from a package server like pypi where potentially binaries are available (splitting) is probably faster than cloning.
more flexibility with splitting (as repos can evolve separately).
IIUC with Unifying Approach we'd set language: rust and pre-commit would cargo install --bins on the fly for our users (even bootstrapping rustup as required). While this is nice, I think we should not do that because we've gone through all the work of making easy to install binaries - we should use them if at all possible.
So that likely leaves us with Splitting Approach, where we need to decide which language to use. A few ideas:
language: R
Create a pseudo-R package at posit-dev/air-pre-commit. The R package would be in charge of downloading the correct binary depending on the platform, and then running it. This would be pretty simple I think, we just need to map the OS and Architecture to the right GitHub Artifact URL
For VERSION, we'd put a constant in the package as VERSION <- 0.4.1 which would correspond to the tag that we release at, so when people set rev: 0.4.1 in their pre-commit-config.yml they get the right version.
I'm not exactly sure how caching of the binary between commits would work here. It sounds like maybe it is up to the R package, and we could use something like rappdirs for the location.
IIUC pre-commit would build the R package the first time around and then reuse the built package for subsequent calls until the tag is updated
language: Python
Be like ruff and compile binary wheels and upload an air python package to pypi that just makes air available. Don't love this idea as it is a lot of extra work on our end for little benefit.
So that likely leaves us with Splitting Approach, where we need to decide which language to use. A few ideas:
With both unifying and splitting, you will point to a git repo and specify a language property, that's not unique to splitting. If you choose language: rust , the git repo pre-commit will install from has to be installable with cargo (into some pre-commit managed directory in your system's cache, not the working directory). So if you have language: rust:
With the unifying approach, the git repo from posit-dev/air is cloned and installed from source. The package has to expose the CLI that you then put into entry:.
With the splitting approach, the rust package at posit-dev/air-pre-commit exists for the mere reason of having a dependency on the package you actually want to use and installing that dependency (with a package manager from a remote package repo) will expose a CLI program like ruff that you then put into entry: field (FWIW for language: r it's not a CLI program, but for many other languages it is). This in my understanding would not work currently, since you don't distribute air through standard rust package repositories (like CRAN for R).
To sum it up: whether or not you choose a splitting or unifying approach, you need to choose a pre-commit language. But if you choose unifying approach and language:r, posit-dev/air has to be a rust and R Package at the same time which brings additional complexity you want to avoid.
On which pre-commit language to use: pre-commit supports standard package management mechanisms for R, rust, python and other languages. If airs usage is limited to the command line and positron / RStudio, you may not see a lot of benefit distributing it through standard package repos like CRAN, pypi etc. {styler} has been used in many different third-party tools that want to format code and I think it was beneficial to have it installable as an R package through standard mechanisms. But that's up to you. In that regard, the fact that {styler} is a pure R implementation is also a benefit. Also, a number of R users don't feel comfortable leaving the R console and an R API was certainly beneficial for {styler}.
If you go for language: r, you get the side benefit of R bindings for other use cases than just pre-commit. As per the docs, the repo needs to use {renv} to manage dependencies (none in your case). As you correctly point out, you would update the tag of the repo in sync with the pointer to the binary in https://github.com/posit-dev/air/releases/download/<VERSION>/air-<ARCH>-<OS>.tar.gz in the R package source code (or just depend on DESCRIPTION to read off the version value that you should keep in sync with the tag).
I'm not exactly sure how caching of the binary between commits would work here. It sounds like maybe it is up to the R package, and we could use something like rappdirs for the location.
There is one virtual environment per hook repo and rev for 2nd class languages and lower, so if you use language: r or language: rust, a new virtual environment is created if rev: changes as a consequence of pre-commit autoupdate. For R specifically, there is also a health check, which means if the R version changes (and R packages installed with previous R version are not available anymore), the repo will be re-installed.
IIUC pre-commit would build the R package the first time around and then reuse the built package for subsequent calls until the tag is updated
Correct.
language: system indeed seems easy but problematic, and language: r is quite slow, which kind of defeats the purpose of air 😀. I'd go for language: rust, I don't think the overhead with rust bootstrapping is that big or complicated for the user, in particular if they use other hooks already (like ruff). Plus it's much easier to manage on your side, in particular with the unified approach.
Also, you can even expose multiple hooks in the hook repo, one with rust and one with system. The entry: can be the same. The user can also override language from .pre-commit-hook.yaml in the hook repo in his .pre-commit-config.yaml but it's not really recommended.
@SamEdwardes without pass_filenames: False, I think you are formatting the working directory and the files staged for commit. I think you probably just want to format the staged files, so probably remove the . in air format .?
Splitting approach
You could also follow ruff and create a repo like posit-dev/air-pre-commit. If you don't publish the crates (which I assume you don't currently according to #98), this approach is not available to you I think, since your air-pre-commit repo can't list air as a dependency that can be resolved and installed by a package manager for rust. Here's how you'd make hook updates available to your users:
This has another advantage: pre-commit always updates to the latest tag, no matter if it's a pre-release or not. By splitting the repos, one can create only tags for stable releases you actually want users to be updated to.
language: Python
Be like ruff and compile binary wheels and upload an air python package to pypi that just makes air available. Don't love this idea as it is a lot of extra work on our end for little benefit.
I think that's a great idea! Also consider that pip is a widely abundant package manager that can be used without admin priviledges. And one could even use uv for installing air on-the-fly: uvx air format .
Why do you think it would be a lot of extra work? In the end it's just one github action that publishes a release on PyPI whenever a tag/release is created on Github.
Stumbled on this via Davis on bluesky, thanks for the comprehensive exploration! One solution that would work for me and my org in particular (but I can understand why this isn't a good global solve) is using language: docker_image.
> git commit -m 'test air'
style files with air.....................................................Failed
- hook id: air-style-files
- files were modified by this hook
> git status
On branch tan/air
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .pre-commit-config.yaml
modified: R/update.R
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: R/update.R
which behaves more like what the original style-files pre-commit hook from lorenzwalthert/precommit does in the sense that it only styles the files that were being committed.
(Yes, I could probably figure out the PATH thing better than just whacking it in the ENTRYPOINT).
A nice thing about supporting this more officially is that it can be versioned to match github release tags and maintained via GHA. It is a little overkill / tiny bit slower than running an installed binary via language: system, but the image is "only" 100MB locally and runs perfectly fine afaict
Our general recommendation is that your team combines Format on Save in your IDE with our GitHub Action, which serves as a "backup check" and a way to ensure external contributions from non-team members are also formatted.
If you are interested in pre-commit, can you help us understand why this doesn't work for you?
I talked with @tanho63 at conf and he said that his team doesn't / isn't allowed to use GitHub Actions, so I understand why it would be useful for him, but I'd be interested in more use cases.
Hi there,
Great question! We are a polyglot data science shop, and the idea is to have alignment with our uv/ruff python setup, which has: VSCode extension for ruff + local pre-commit hooks + GHA check that uses the same pre-commit hooks.
On Mon, Sep 22, 2025 at 10:16 AM, Davis Vaughan < ***@***.*** > wrote:
*DavisVaughan* left a comment (posit-dev/ air#269) (
#269 (comment) )
Our general recommendation is that your team combines Format on Save in
your IDE with our GitHub Action ( https://github.com/posit-dev/setup-air )
, which serves as a "backup check" and a way to ensure external
contributions from non-team members are also formatted.
If you are interested in pre-commit, can you help us understand why this
doesn't work for you?
I talked with @ tanho63 ( https://github.com/tanho63 ) at conf and he said
that his team doesn't / isn't allowed to use GitHub Actions, so I
understand why it would be useful for him, but I'd be interested in more
use cases.
—
Reply to this email directly, view it on GitHub (
#269 (comment) ) , or
unsubscribe (
https://github.com/notifications/unsubscribe-auth/AAECJCKLFSRRZXI2TI5P7QL3UAAFRAVCNFSM6AAAAABYACZ5IKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTGMJZGMYTEMBTGM
).
You are receiving this because you commented. Message ID: <posit-dev/air/issues/269/3319312033
@ github. com>
Would any brave souls be willing to try this in their .pre-commit-config.yaml? It seems to work for me when I add it to dplyr, change some files, and run pre-commit locally.
repos:
- repo: https://github.com/posit-dev/air-pre-commit# Air versionrev: 0.8.2hooks:
# Run the formatter
- id: air-format
We distribute air as a package on PyPI now https://pypi.org/project/air-formatter/ (it's literally just the air binary, no python code in there). While this is odd, it means we can tap into pre-commit's great support for downloading PyPI packages on the fly.
As a side benefit you can also invoke air via uv now, which is pretty awesome
uvx --from air-formatter air --help
Reacted by Arthur Bréant, Julia Silge, Tan Ho, Damon Bayer, Lorenz Walthert and Kelly Sovacool, PhD
I tried to set up what you are suggesting above, and it seems to work:
[r-project-template]$ just pre-commit
uvx pre-commit run --all-files --hook-stage push
air format...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
check for added large files..............................................Passed
check for merge conflicts................................................Passed
check yaml...............................................................Passed
check toml...............................................................Passed
check json...............................................................Passed
pretty format json.......................................................Passed
codespell................................................................Passed
I agree it is kind of strange that uvx is required to run these.
This worked as expected (and is drop-in compatible with prek as well)! Does air automatically only format recognized file extensions e.g. .R/.Rmd/.qmd or do we also need to supply the yaml param files: [.R|.r|.Rmd|.qmd] ?
@tanho63 air does not support Rmd nor Qmd from the command line. We don't currently have a quarto parser for that, but one is in the works. It will only format R files, should do .R and .r IIUC.
Thanks so much, this is gonna be super useful! I think it might be helpful if the doc for integrating in pre-commit linked the list of github release tags so that users know exactly what values are actually possible to pin to, i.e.
And, fwiw @tanho63@DavisVaughan you may want to check out panache, which is aiming to be the (precommitable) linter / formatter for quarto, and which can already delegate to air for R cells (as well as ruff for python cells, other tools for javascript, etc.)
Ultimately it may make for sense to have folks use a quarto linter/reformatter that uses air, rather than having air reformat qmd files, and only R qmd files at that.
Ultimately it may make for sense to have folks use a quarto linter/reformatter that uses air
Yea I tend to agree, since Air would never be able to help with the Python chunks if you have a mixed R/Python qmd.
Quarto itself will likely have something like this eventually, though I cant promise any timeline at all. A rust based quarto parser is going to make that feasible though https://github.com/quarto-dev/q2
This worked as expected (and is drop-in compatible with prek as well)! Does air automatically only format recognized file extensions e.g. .R/.Rmd/.qmd or do we also need to supply the yaml param files: [.R|.r|.Rmd|.qmd] ?
Activity
lorenzwalthert commentedon Feb 28, 2025
@DavisVaughan your observations are all correct IMO. Python must not necessarily be available, you can also install pre-commit with brew for example, then it will manage the python installation I think. Note that pre-commit always clones a repo using a tag or a hash from
rev:field in the.pre-commit-config.yaml, which is then installed (with all its dependencies).Unifying approach
Hence, you could also place the
.pre-commit-config.yamlfile in the posit-dev/air repo directly. Then, if you push new tags to this repo (which I guess you do anyways if you release a new version),pre-commit autoupdatewill recognise them andpre-commit autoupdatewill pick it up. Notepre-commit autoupdate --bleeding-edgeor so uses always the latest commit hash from the default branch, but few people seem to use that.Splitting approach
You could also follow ruff and create a repo like posit-dev/air-pre-commit. If you don't publish the crates (which I assume you don't currently according to #98), this approach is not available to you I think, since your air-pre-commit repo can't list air as a dependency that can be resolved and installed by a package manager for rust. Here's how you'd make hook updates available to your users:
pre-commit autoupdatefetches the latest version of the hook.There are many examples where the pre-commit hooks configs are directly in the root of the actual project. I'd say considerations pro and con of unifying vs splitting:
DavisVaughan commentedon Feb 28, 2025
Noting that what we really need is something like this pre-commit/pre-commit#1453 to download pre installed binaries using pre-commit
DavisVaughan commentedon Feb 28, 2025
IIUC with Unifying Approach we'd set
language: rustand pre-commit wouldcargo install --binson the fly for our users (even bootstrapping rustup as required). While this is nice, I think we should not do that because we've gone through all the work of making easy to install binaries - we should use them if at all possible.So that likely leaves us with Splitting Approach, where we need to decide which
languageto use. A few ideas:language: R
Create a pseudo-R package at
posit-dev/air-pre-commit. The R package would be in charge of downloading the correct binary depending on the platform, and then running it. This would be pretty simple I think, we just need to map the OS and Architecture to the right GitHub Artifact URLFor
VERSION, we'd put a constant in the package asVERSION <- 0.4.1which would correspond to thetagthat we release at, so when people setrev: 0.4.1in their pre-commit-config.yml they get the right version.I'm not exactly sure how caching of the binary between commits would work here. It sounds like maybe it is up to the R package, and we could use something like rappdirs for the location.
IIUC pre-commit would build the R package the first time around and then reuse the built package for subsequent calls until the
tagis updatedlanguage: Python
Be like ruff and compile binary wheels and upload an air python package to pypi that just makes air available. Don't love this idea as it is a lot of extra work on our end for little benefit.
language: system
https://pre-commit.com/#system
We could write a "system" hook that assumes that the user has already installed the binary. In this case:
posit-dev/airrepo (but there are still arguments for keeping it separate, lightweight clone times for example)airbinary already be installedI don't mind this that much, and if
language: downloadever gets implemented we could switch to that.lorenzwalthert commentedon Feb 28, 2025
With both unifying and splitting, you will point to a git repo and specify a language property, that's not unique to splitting. If you choose
language: rust, the git repo pre-commit will install from has to be installable with cargo (into some pre-commit managed directory in your system's cache, not the working directory). So if you havelanguage: rust:entry:.ruffthat you then put intoentry:field (FWIW forlanguage: rit's not a CLI program, but for many other languages it is). This in my understanding would not work currently, since you don't distribute air through standard rust package repositories (like CRAN for R).To sum it up: whether or not you choose a splitting or unifying approach, you need to choose a pre-commit language. But if you choose unifying approach and
language:r, posit-dev/air has to be a rust and R Package at the same time which brings additional complexity you want to avoid.lorenzwalthert commentedon Feb 28, 2025
On which pre-commit language to use: pre-commit supports standard package management mechanisms for R, rust, python and other languages. If airs usage is limited to the command line and positron / RStudio, you may not see a lot of benefit distributing it through standard package repos like CRAN, pypi etc. {styler} has been used in many different third-party tools that want to format code and I think it was beneficial to have it installable as an R package through standard mechanisms. But that's up to you. In that regard, the fact that {styler} is a pure R implementation is also a benefit. Also, a number of R users don't feel comfortable leaving the R console and an R API was certainly beneficial for {styler}.
If you go for
language: r, you get the side benefit of R bindings for other use cases than just pre-commit. As per the docs, the repo needs to use {renv} to manage dependencies (none in your case). As you correctly point out, you would update the tag of the repo in sync with the pointer to the binary inhttps://github.com/posit-dev/air/releases/download/<VERSION>/air-<ARCH>-<OS>.tar.gzin the R package source code (or just depend on DESCRIPTION to read off the version value that you should keep in sync with the tag).There is one virtual environment per hook repo and
revfor 2nd class languages and lower, so if you uselanguage: rorlanguage: rust, a new virtual environment is created ifrev:changes as a consequence ofpre-commit autoupdate. For R specifically, there is also a health check, which means if the R version changes (and R packages installed with previous R version are not available anymore), the repo will be re-installed.Correct.
language: systemindeed seems easy but problematic, andlanguage: ris quite slow, which kind of defeats the purpose of air 😀. I'd go forlanguage: rust, I don't think the overhead with rust bootstrapping is that big or complicated for the user, in particular if they use other hooks already (like ruff). Plus it's much easier to manage on your side, in particular with the unified approach.Also, you can even expose multiple hooks in the hook repo, one with rust and one with system. The
entry:can be the same. The user can also overridelanguagefrom.pre-commit-hook.yamlin the hook repo in his.pre-commit-config.yamlbut it's not really recommended.lorenzwalthert commentedon Feb 28, 2025
Sorry my comments get so long. Hope they are not too convoluted.
SamEdwardes commentedon Mar 20, 2025
In lieu of an official pre-commit hook, you can use this if you already have air installed:
lorenzwalthert commentedon Mar 21, 2025
@SamEdwardes without
pass_filenames: False, I think you are formatting the working directory and the files staged for commit. I think you probably just want to format the staged files, so probably remove the.inair format .?SamEdwardes commentedon Mar 22, 2025
Thank you for the suggestion! I am new to pre-commit and appreciate the feedback.
grst commentedon Apr 24, 2025
This has another advantage: pre-commit always updates to the latest tag, no matter if it's a pre-release or not. By splitting the repos, one can create only tags for stable releases you actually want users to be updated to.
I think that's a great idea! Also consider that pip is a widely abundant package manager that can be used without admin priviledges. And one could even use
uvfor installingairon-the-fly:uvx air format .Why do you think it would be a lot of extra work? In the end it's just one github action that publishes a release on PyPI whenever a tag/release is created on Github.
tanho63 commentedon Jul 25, 2025
Stumbled on this via Davis on bluesky, thanks for the comprehensive exploration! One solution that would work for me and my org in particular (but I can understand why this isn't a good global solve) is using
language: docker_image.Minimal Dockerfile here:
and usage like:
gives me
which behaves more like what the original
style-filespre-commit hook from lorenzwalthert/precommit does in the sense that it only styles the files that were being committed.(Yes, I could probably figure out the PATH thing better than just whacking it in the ENTRYPOINT).
A nice thing about supporting this more officially is that it can be versioned to match github release tags and maintained via GHA. It is a little overkill / tiny bit slower than running an installed binary via
language: system, but the image is "only" 100MB locally and runs perfectly fine afaictjpvelez commentedon Sep 21, 2025
Just wanted to chime on as a user that this would be super valuable!
DavisVaughan commentedon Sep 22, 2025
Our general recommendation is that your team combines
Format on Savein your IDE with our GitHub Action, which serves as a "backup check" and a way to ensure external contributions from non-team members are also formatted.If you are interested in pre-commit, can you help us understand why this doesn't work for you?
I talked with @tanho63 at conf and he said that his team doesn't / isn't allowed to use GitHub Actions, so I understand why it would be useful for him, but I'd be interested in more use cases.
jpvelez commentedon Sep 22, 2025
DavisVaughan commentedon Sep 22, 2025
So you don't use https://github.com/astral-sh/ruff-action?
13 remaining items
DavisVaughan commentedon Feb 27, 2026
Would any brave souls be willing to try this in their
.pre-commit-config.yaml? It seems to work for me when I add it to dplyr, change some files, and runpre-commitlocally.We distribute air as a package on PyPI now https://pypi.org/project/air-formatter/ (it's literally just the air binary, no python code in there). While this is odd, it means we can tap into pre-commit's great support for downloading PyPI packages on the fly.
As a side benefit you can also invoke air via uv now, which is pretty awesome
DavisVaughan commentedon Feb 27, 2026
In particular, @tanho63 since we talked about this a few months back!
tanho63 commentedon Feb 27, 2026
First thing Monday!
novica commentedon Feb 28, 2026
Hi @DavisVaughan
I am trying to recreate a python-project-template for R, and I reached the pre-commit task.
I tried to set up what you are suggesting above, and it seems to work:
I agree it is kind of strange that
uvxis required to run these.DavisVaughan commentedon Feb 28, 2026
@novica uvx is not required to run the pre-commit hook
novica commentedon Feb 28, 2026
@DavisVaughan yes. I was not being precise in my wording.
I think I need to run
uvxto parse the yaml with pre-commit to have the hook set up. Or maybe I am doing an extra step -- but it works :)tanho63 commentedon Mar 3, 2026
This worked as expected (and is drop-in compatible with prek as well)! Does air automatically only format recognized file extensions e.g.
.R/.Rmd/.qmdor do we also need to supply the yaml paramfiles: [.R|.r|.Rmd|.qmd]?DavisVaughan commentedon Mar 3, 2026
@tanho63 air does not support Rmd nor Qmd from the command line. We don't currently have a quarto parser for that, but one is in the works. It will only format R files, should do
.Rand.rIIUC.DavisVaughan commentedon Mar 5, 2026
I'm going to call this closed then! Use air-pre-commit as described here https://github.com/posit-dev/air-pre-commit
tanho63 commentedon Mar 5, 2026
Thanks so much, this is gonna be super useful! I think it might be helpful if the doc for integrating in pre-commit linked the list of github release tags so that users know exactly what values are actually possible to pin to, i.e.
air/docs/integration-pre-commit.qmd
Lines 22 to 24 in c6e01e7
should link to https://github.com/posit-dev/air-pre-commit/releases
jpvelez commentedon Mar 9, 2026
This is working great!
And, fwiw @tanho63 @DavisVaughan you may want to check out panache, which is aiming to be the (precommitable) linter / formatter for quarto, and which can already delegate to air for R cells (as well as ruff for python cells, other tools for javascript, etc.)
Ultimately it may make for sense to have folks use a quarto linter/reformatter that uses air, rather than having air reformat qmd files, and only R qmd files at that.
DavisVaughan commentedon Mar 9, 2026
Yea I tend to agree, since Air would never be able to help with the Python chunks if you have a mixed R/Python qmd.
Quarto itself will likely have something like this eventually, though I cant promise any timeline at all. A rust based quarto parser is going to make that feasible though https://github.com/quarto-dev/q2
And some discussion https://github.com/orgs/quarto-dev/discussions/12082
jpvelez commentedon Mar 10, 2026
@DavisVaughan I wasn't aware of q2! Exciting times.
maikol-solis commentedon Apr 7, 2026
@tanho63 You need this instead: https://github.com/jolars/panache