Add optional uv backend (opt-in via --use-uv / config)#454
Conversation
When a `uv` binary is found in PATH, fades now uses it as a faster backend to create the virtualenv (`uv venv`) and install dependencies (`uv pip install`), instead of stdlib `venv` + `pip`. Detection is via `helpers.get_uv_exe()` (shutil.which); fades does not depend on the `uv` PyPI package, since it runs against the system Python. The new `UvManager` mirrors `PipManager`'s interface (install/get_version/ freeze) so it plugs into the existing dispatch in `envbuilder.create_venv`. A `--no-uv` flag forces the classic pip backend even when uv is available. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Benchmark: uv vs pipWall time for
|
| Scenario | Backend | Wall time |
|---|---|---|
| Fresh venv (create + install) | pip (--no-uv) |
3.43s |
| Fresh venv (create + install) | uv | 0.39s |
| Existing/cached venv (no install) | either | ~0.16s |
≈ 8.7× faster on the create+install path.
jupyterlab (~60 transitive deps)
| Scenario | Backend | Wall time |
|---|---|---|
| Fresh, cold package cache (incl. downloads) | pip (--no-uv) |
16.63s |
| Fresh, cold package cache (incl. downloads) | uv | 12.70s |
| Fresh, warm package cache (already downloaded) | pip (--no-uv) |
13.97s |
| Fresh, warm package cache (already downloaded) | uv | 1.62s |
| Existing/cached venv (no install) | either | 0.92s |
- Cold cache (first-ever install): ≈ 1.3×. On a first install both backends spend most of the time downloading the same ~60 wheels over the network, which dominates and masks the backend difference.
- Warm cache (steady state, packages already fetched once): 13.97s → 1.62s ≈ 8.6× faster — this isolates uv's actual resolve+install speed and matches the django ratio.
Takeaways
- uv is ~8–9× faster on the install itself; the small cold-cache jupyterlab number is just because raw download bandwidth (identical for both backends) dominates the very first run.
- The cached-venv path is unchanged regardless of backend — no install happens, fades just reuses the venv.
Single run per row, warm/cold as noted; numbers will vary with network and machine.
- freeze: drop `uv pip freeze --quiet`, which empties the whole output on some uv versions (producing a 0-byte freeze file); filter uv's info line instead, mirroring get_version. - freeze: pick the backend from the venv's `pip_installed` metadata, not the current run's flags. A uv-built venv has no pip, and the cache key doesn't include the backend, so it can be reused under --no-uv / when uv is gone; PipManager.freeze would then crash on a missing pip. - uv `--python`: probe for `python` / `python.exe` so the uv backend works on Windows (uv treats --python as a literal path, no PATHEXT resolution). - main: fall back to the pip backend when `--venv-options` contains flags `uv venv` doesn't accept (e.g. --symlinks), keeping documented usage working. - CI: make the --no-uv check also assert fades' own exit status (a crash could silently pass the negated grep), and add a --freeze regression step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Install uv in the native-windows job and add the uv auto-detect, --no-uv, and --freeze checks there. This confirms the uv path works on Windows, where the venv interpreter is Scripts\python.exe (regression coverage for the uv --python resolution). The grep-based steps run under bash (Git Bash), since the Windows runner defaults to PowerShell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pip-installed uv wasn't reliably on PATH for fades' shutil.which on the Windows runner, so the auto-detect step couldn't confirm the uv backend. Use the official astral-sh/setup-uv action to guarantee uv on PATH, and capture + print fades output so failures in the uv path are diagnosable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Windows interpreter path was interpolated raw into the bash steps, where unquoted backslashes were stripped (command not found). Single-quote it and convert backslashes to forward slashes (accepted by Git Bash) so the uv path is actually exercised on Windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uv seeds nothing by default, so uv-built venvs lacked pip and setuptools that the classic 'python -m venv' backend provides, breaking packages that expect them present. Pass --seed so the venv ships pip (+ setuptools, per the Python version, matching stdlib venv) at negligible cost. Note: this does not restore pkg_resources for packages like azure-cli, since uv seeds a recent setuptools (>=81) that removed it -- the same is true for the pip backend on Python 3.12+. Documented the 'setuptools<81' workaround. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uv venvs now seed pip/setuptools (
|
Per discussion on #451, uv is no longer auto-detected/used just because it's on PATH (it produces different venvs than pip, so behaviour shouldn't change based on an external binary's presence). uv is now explicit: - `--use-uv` enables it; `--uv-path PATH` points at a specific uv (implies --use-uv); without a path uv is looked up in PATH and validated. If uv is requested but not found, fades errors out. - `use_uv=true` in any fades config file enables it permanently (machine/user/ repo `.fades.ini`); a CLI flag still wins. - `--pip-options`/`--venv-options` together with --use-uv is an error; new `--uv-pip-options` passes options to `uv pip install`. - the PyPI availability pre-check is skipped under uv (uv resolves directly). - `--freeze` uses plain pip freeze (uv venvs are seeded with pip), dropping the uv-specific freeze code. Detection/resolution lives in main._resolve_uv_backend (unit-tested); README and man page document the opt-in model and how to enable it always via config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switched uv from default to opt-inFollowing the discussion in #451 (thanks @facundobatista), uv is no longer auto-detected/used when present. It's now explicit:
The All green on Linux/macOS/Windows. |
Wrap get_confdir() in Path() so the one-liner works whether get_confdir returns a str (older fades) or a Path, and add -W ignore to silence the pkg_resources deprecation warning emitted by older installed versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
facundobatista
left a comment
There was a problem hiding this comment.
Great work! I annotated some questions and details. Thanks!
| @@ -1 +1 @@ | |||
| # Copyright 2014-2026 Facundo Batista, Nicolás Demarchi | |||
There was a problem hiding this comment.
Remember updating copyright years.
| parser.add_argument( | ||
| '--uv-pip-options', action='append', default=[], | ||
| help="extra options to be supplied to 'uv pip install' (this option can be used multiple " | ||
| "times); only valid together with --use-uv.") |
There was a problem hiding this comment.
Please rephrase this (as it also may be used if --uv-path is included). Maybe saying something like "only valid if 'uv' is indicated to use"?
| @@ -0,0 +1,86 @@ | |||
| # Copyright 2024-2026 Facundo Batista, Nicolás Demarchi | |||
|
|
||
| .TP | ||
| .BR --uv-pip-options =\fIUV_PIP_OPTION\fR | ||
| Extra options to be supplied to \fBuv pip install\fR (this option can be used multiple times); only valid together with \fB--use-uv\fR. |
There was a problem hiding this comment.
Same detail for the text as I commented in argparse above.
| @@ -0,0 +1,125 @@ | |||
| # Copyright 2024-2026 Facundo Batista, Nicolás Demarchi | |||
|
|
||
| - fades runs ``uv venv --seed``, so the virtual environment ships ``pip`` (and ``setuptools``, depending on the Python version) just like the classic ``venv`` backend, keeping packages that expect them present working. | ||
| - The PyPI availability pre-check is skipped (``uv`` resolves directly and fails early on its own). | ||
| - ``--pip-options`` and ``--venv-options`` are **not** valid together with ``--use-uv`` (they target ``pip``/``venv``); use ``--uv-pip-options`` to pass extra options to ``uv pip install`` instead. |
There was a problem hiding this comment.
change "together with --use-uv" for something like "when uv indicated to use", aligned with argparse/man texts.
| Enabling uv permanently (via config) | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| If you always want the uv backend, set ``use_uv=true`` under the ``[fades]`` section of any fades config file. fades reads (in increasing priority) ``/etc/fades/fades.ini`` (machine-wide), the per-user config (``<xdg-config>/fades/fades.ini``) and a repo-local ``./.fades.ini`` (handy to enable uv for a single project). A ``--use-uv``/``--no``-style flag on the command line still wins over the config. |
There was a problem hiding this comment.
No need to explain again how the config files work, just refer to "Setting options using config files" section.
| python -W ignore -c "import configparser; from pathlib import Path; from fades.helpers import get_confdir; \ | ||
| p = Path(get_confdir()) / 'fades.ini'; c = configparser.ConfigParser(); c.read(p); \ | ||
| c.has_section('fades') or c.add_section('fades'); c.set('fades', 'use_uv', 'true'); \ | ||
| c.write(p.open('w')); print('uv enabled by default in', p)" |
There was a problem hiding this comment.
I have mixed feelings about this code block...
- "editing files by hand" is a problem for a
fadesuser? - if we leave this recommendation on all config parameters, the README is a mess
- the code block is quite ugly (not sure that it can be improved without losing portability)
- maybe something like this corresponds to the "Setting options using config files" section?
|
|
||
|
|
||
| def create_venv(requested_deps, interpreter, is_current, options, pip_options, avoid_pip_upgrade, | ||
| use_uv=False, uv_exe=None, uv_pip_options=None): |
There was a problem hiding this comment.
I don't like this function's interface. It's weird (bunch of options together not working with other bunch of options). It's hard to check all combinations (e.g. having uv_exe if use_uv, but also that avoid_pip_upgrade is not needed, etc).
I suggest the following refactor:
- have two entry points:
create_venv_classicandcreate_venv_uv - the caller in
main.pyis responsible of deciding which to use- this will make the interface of each function simpler and will make sense
- no need to weird verifications
- each function will do a little of work and will call a common one
- this common one would be completely agnostic
- specially if it receives a "manager" (UvManager or PipManager instance, according to the caller)
What do you think?
| installed inside a venv), so we rely on a ``uv`` *binary* rather than the ``uv`` PyPI package, | ||
| which would require polluting the system Python. | ||
| """ | ||
| return shutil.which(uv_path) if uv_path else shutil.which("uv") |
There was a problem hiding this comment.
I wonder if we should log in DEBUG here, for eventually be sure which uv is being used...
What
Adds
uvas an opt-in backend for venv creation and dependency installs. By default fades keeps usingvenv/pip; uv is used only when explicitly requested. Everything else (dependency parsing, venv indexing/cache, GC,--where/--rm, REPL/autoimport, config files) is unchanged.Closes #451.
Enabling uv
--use-uv— use uv for this run.--uv-path PATH— point at a specific uv executable (implies--use-uv); otherwise uv is looked up inPATHand validated. If uv is requested but not found, fades errors out.use_uv=truein any fades config file (/etc/fades/fades.ini, the per-userfades.ini, or a repo-local./.fades.ini) enables it permanently; a CLI flag still wins.Design notes
PipManager. fades does not depend on theuvPyPI package (it runs against the system Python); it uses a uv binary on PATH.UvManagermirrorsPipManager'sinstall/get_version, dispatched fromenvbuilder.create_venv. Backend resolution + option validation live inmain._resolve_uv_backend(unit-tested).uv venv --seedso the venv ships pip (and setuptools, per Python version) like the classic backend.--pip-options/--venv-optionsare errors (use--uv-pip-options);--freezeuses plain pip freeze on the seeded venv.Tests / CI
_resolve_uv_backend(enabled/disabled,--uv-path, not-found, option conflicts),UvManager, envbuilder dispatch,get_uv_exe.--use-uv= uv, conflict + bogus-path errors, and--freezeon a uv venv. Windows confirms the uv path end-to-end.🤖 Generated with Claude Code