-
Notifications
You must be signed in to change notification settings - Fork 44
Add optional uv backend (opt-in via --use-uv / config) #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
8759be6
00592a9
2a6dc3a
b93fcd1
af1bda7
85db371
9ab2790
7b47250
9e1cfa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -391,6 +391,42 @@ Examples: | |
| ``fades --python-options=-B foo.py`` | ||
|
|
||
|
|
||
| Using uv as backend | ||
| ------------------- | ||
|
|
||
| fades can optionally use `uv <https://github.com/astral-sh/uv>`_ as a (much faster) backend to create the virtual environment and install the dependencies, instead of ``venv`` and ``pip``. This is **opt-in**: fades does not change its behaviour just because ``uv`` happens to be installed, because ``uv`` produces slightly different virtual environments than ``pip``. | ||
|
|
||
| Enable it per run with ``--use-uv``:: | ||
|
|
||
| fades --use-uv -d requests -x python | ||
|
|
||
| ``uv`` must be available: fades looks it up in ``PATH`` (installed via your system package manager, the standalone installer, etc.; fades does not depend on the ``uv`` PyPI package). You can point at a specific executable with ``--uv-path /path/to/uv`` (which also implies ``--use-uv``). If ``uv`` is requested but can't be found, fades errors out. | ||
|
|
||
| Everything else stays fades: dependency parsing (``# fades`` comments, docstring, ``-d``/``-r``), venv indexing and reuse, ``--where``/``--rm``/``--clean-unused-venvs``, config files, the REPL and ``--autoimport``, etc. | ||
|
|
||
| Some notes when using ``uv``: | ||
|
|
||
| - 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. | ||
|
|
||
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to explain again how the config files work, just refer to "Setting options using config files" section. |
||
|
|
||
| To turn it on for your user without editing files by hand, run:: | ||
|
|
||
| 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)" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have mixed feelings about this code block...
|
||
|
|
||
| A note about ``pkg_resources``: some packages (e.g. ``azure-cli``) still import the long-deprecated ``pkg_resources``, which was **removed** in ``setuptools >= 81``. uv (and the classic backend on Python 3.12+) installs a recent ``setuptools`` that no longer provides it, so those imports fail. This is not specific to the uv backend; if you hit it, add an older setuptools as an explicit dependency:: | ||
|
|
||
| fades --use-uv -d azure-cli -d "setuptools<81" -x az --help | ||
|
|
||
|
|
||
| Setting options using config files | ||
| ---------------------------------- | ||
|
|
||
|
|
@@ -479,9 +515,9 @@ Execute the Python interactive interpreter in a virtual environment after instal | |
|
|
||
| fades -r requirements.txt -r requirements_devel.txt | ||
|
|
||
| Use the ``django-admin.py`` script to start a new project named ``foo``, without having to have django previously installed:: | ||
| Use the ``django-admin`` script to start a new project named ``foo``, without having to have django previously installed:: | ||
|
|
||
| fades -d django -x django-admin.py startproject foo | ||
| fades -d django -x django-admin startproject foo | ||
|
|
||
| Remove a virtual environment matching the given uuid from disk and cache index:: | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| import logging | ||
| import os | ||
| import shutil | ||
| import sys | ||
| from datetime import datetime, timezone | ||
| from pathlib import Path | ||
| from uuid import uuid4 | ||
|
|
@@ -27,6 +28,7 @@ | |
| from fades import FadesError, REPO_PYPI, REPO_VCS | ||
| from fades import helpers, cache | ||
| from fades.pipmanager import PipManager | ||
| from fades.uvmanager import UvManager | ||
| from fades.multiplatform import filelock | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -115,11 +117,49 @@ def post_setup(self, context): | |
| self.env_bin_path = Path(context.bin_path) | ||
|
|
||
|
|
||
| def create_venv(requested_deps, interpreter, is_current, options, pip_options, avoid_pip_upgrade): | ||
| """Create a new virtualvenv with the requirements of this script.""" | ||
| def create_with_uv(interpreter, is_current, venv_options, uv_exe): | ||
| """Create a virtual environment using uv, returning its path and bin path.""" | ||
| env_path = helpers.get_basedir() / str(uuid4()) | ||
| logger.debug("Env will be created at: %s (with uv)", env_path) | ||
|
|
||
| # when there's no explicitly requested interpreter (current Python), point uv at fades' | ||
| # own interpreter so the venv matches what the cache was keyed on | ||
| python = interpreter if (interpreter is not None and not is_current) else sys.executable | ||
| # '--seed' installs pip + setuptools (+ wheel) like the classic 'python -m venv' backend, | ||
| # so packages that expect those to be present in the venv keep working (uv seeds nothing | ||
| # by default); the cost is negligible | ||
| args = [uv_exe, "venv", "--seed", str(env_path), "--python", str(python)] | ||
| args.extend(venv_options) | ||
|
|
||
| try: | ||
| helpers.logged_exec(args) | ||
| except helpers.ExecutionError as error: | ||
| error.dump_to_log(logger) | ||
| raise FadesError("Failed to create virtual environment with uv") | ||
| except Exception as error: | ||
| logger.exception("Error creating virtual environment with uv: %s", error) | ||
| raise FadesError("General error while creating venv with uv") | ||
|
|
||
| env_bin_path = helpers.get_env_bin_path(env_path) | ||
| return env_path, env_bin_path | ||
|
|
||
|
|
||
| def create_venv(requested_deps, interpreter, is_current, options, pip_options, avoid_pip_upgrade, | ||
| use_uv=False, uv_exe=None, uv_pip_options=None): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 I suggest the following refactor:
What do you think? |
||
| """Create a new virtualvenv with the requirements of this script. | ||
|
|
||
| When ``use_uv`` is True the caller must provide ``uv_exe`` (the resolved uv binary). | ||
| """ | ||
| # create virtual environment | ||
| env = _FadesEnvBuilder() | ||
| env_path, env_bin_path, pip_installed = env.create_env(interpreter, is_current, options) | ||
| if use_uv: | ||
| logger.debug("Creating virtual environment with uv backend") | ||
| env_path, env_bin_path = create_with_uv( | ||
| interpreter, is_current, options['venv_options'], uv_exe) | ||
| # 'uv venv --seed' installs pip in the venv, so it's available like in the classic path | ||
| pip_installed = True | ||
| else: | ||
| env = _FadesEnvBuilder() | ||
| env_path, env_bin_path, pip_installed = env.create_env(interpreter, is_current, options) | ||
| venv_data = {} | ||
| venv_data['env_path'] = env_path | ||
| venv_data['env_bin_path'] = env_bin_path | ||
|
|
@@ -129,9 +169,12 @@ def create_venv(requested_deps, interpreter, is_current, options, pip_options, a | |
| installed = {} | ||
| for repo in requested_deps.keys(): | ||
| if repo in (REPO_PYPI, REPO_VCS): | ||
| mgr = PipManager( | ||
| env_bin_path, pip_installed=pip_installed, options=pip_options, | ||
| avoid_pip_upgrade=avoid_pip_upgrade) | ||
| if use_uv: | ||
| mgr = UvManager(env_bin_path, options=uv_pip_options, uv_exe=uv_exe) | ||
| else: | ||
| mgr = PipManager( | ||
| env_bin_path, pip_installed=pip_installed, options=pip_options, | ||
| avoid_pip_upgrade=avoid_pip_upgrade) | ||
| else: | ||
| logger.warning("Install from %r not implemented", repo) | ||
| continue | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| import json | ||
| import logging | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
|
|
@@ -88,6 +89,17 @@ def logged_exec(cmd): | |
| return stdout | ||
|
|
||
|
|
||
| def get_uv_exe(uv_path=None): | ||
| """Return the path to the ``uv`` binary, or None if it can't be found. | ||
|
|
||
| If ``uv_path`` is given that exact location is validated (it must exist and be executable), | ||
| otherwise ``uv`` is looked up in PATH. fades runs against the system Python (it is not | ||
| 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") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should log in DEBUG here, for eventually be sure which |
||
|
|
||
|
|
||
| def _get_basedirectory(): | ||
| from xdg import BaseDirectory | ||
| return BaseDirectory | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -223,6 +223,33 @@ def detect_inside_virtualenv(prefix, real_prefix, base_prefix): | |
| return prefix != base_prefix | ||
|
|
||
|
|
||
| def _resolve_uv_backend(args): | ||
| """Decide whether to use the uv backend, validating the related options. | ||
|
|
||
| Return a tuple ``(use_uv, uv_exe, uv_pip_options)``. uv is used only when explicitly | ||
| requested (``--use-uv``, ``--uv-path`` or ``use_uv`` in a config file). Raise FadesError if | ||
| uv is requested but can't be found, or if incompatible options were given. | ||
| """ | ||
| use_uv = bool(args.use_uv or args.uv_path) | ||
| if not use_uv: | ||
| return False, None, [] | ||
|
|
||
| uv_exe = helpers.get_uv_exe(args.uv_path) | ||
| if not uv_exe: | ||
| msg = ("uv was requested (--use-uv) but no usable uv binary was found; install uv or " | ||
| "indicate its location with --uv-path") | ||
| logger.error(msg) | ||
| raise FadesError(msg) | ||
|
|
||
| if args.pip_options or args.venv_options: | ||
| msg = ("--pip-options/--venv-options can't be used together with --use-uv; " | ||
| "use --uv-pip-options to pass options to uv") | ||
| logger.error(msg) | ||
| raise FadesError(msg) | ||
|
|
||
| return True, uv_exe, args.uv_pip_options | ||
|
|
||
|
|
||
| def go(): | ||
| """Make the magic happen.""" | ||
| parser = argparse.ArgumentParser( | ||
|
|
@@ -281,6 +308,18 @@ def go(): | |
| '--avoid-pip-upgrade', action='store_true', | ||
| help="disable the automatic pip upgrade that happens after the virtualenv is created " | ||
| "and before the dependencies begin to be installed.") | ||
| parser.add_argument( | ||
| '--use-uv', action='store_true', | ||
| help="use uv (instead of venv/pip) to create the virtualenv and install dependencies; " | ||
| "uv must be available in PATH or indicated with --uv-path.") | ||
| parser.add_argument( | ||
| '--uv-path', action='store', metavar='UV_PATH', | ||
| help="path to the uv executable to use (implies --use-uv); if not given, uv is looked up " | ||
| "in PATH.") | ||
| 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.") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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"? |
||
|
|
||
| mutexg = parser.add_mutually_exclusive_group() | ||
| mutexg.add_argument( | ||
|
|
@@ -399,6 +438,11 @@ def go(): | |
| if args.system_site_packages: | ||
| options['venv_options'].append("--system-site-packages") | ||
|
|
||
| # use uv as the backend only when explicitly requested (flag, --uv-path or config) | ||
| use_uv, uv_exe, uv_pip_options = _resolve_uv_backend(args) | ||
| if use_uv: | ||
| logger.debug("Using uv backend found at %r", uv_exe) | ||
|
|
||
| create_venv = False | ||
| venv_data = venvscache.get_venv(indicated_deps, interpreter, uuid, options) | ||
| if venv_data: | ||
|
|
@@ -412,8 +456,9 @@ def go(): | |
| create_venv = True | ||
|
|
||
| if create_venv: | ||
| # Check if the requested packages exists in pypi. | ||
| if not args.no_precheck_availability and indicated_deps.get('pypi'): | ||
| # Check if the requested packages exists in pypi. uv resolves directly and fails early | ||
| # by itself, so this extra check is skipped when using the uv backend. | ||
| if not args.no_precheck_availability and not use_uv and indicated_deps.get('pypi'): | ||
| logger.info( | ||
| "Checking the availabilty of dependencies in PyPI. " | ||
| "You can use '--no-precheck-availability' to avoid it.") | ||
|
|
@@ -423,7 +468,8 @@ def go(): | |
|
|
||
| # Create a new venv | ||
| venv_data, installed = envbuilder.create_venv( | ||
| indicated_deps, args.python, is_current, options, pip_options, args.avoid_pip_upgrade) | ||
| indicated_deps, args.python, is_current, options, pip_options, | ||
| args.avoid_pip_upgrade, use_uv, uv_exe, uv_pip_options) | ||
| # store this new venv in the cache | ||
| venvscache.store(installed, venv_data, interpreter, options) | ||
|
|
||
|
|
@@ -433,7 +479,9 @@ def go(): | |
| return 0 | ||
|
|
||
| if args.freeze: | ||
| # beyond all the rest of work, dump the dependencies versions to a file | ||
| # beyond all the rest of work, dump the dependencies versions to a file; all venvs have | ||
| # pip available (the uv backend seeds it via 'uv venv --seed'), so a plain pip freeze | ||
| # gives a consistent requirements format regardless of the backend used | ||
| mgr = pipmanager.PipManager(venv_data['env_bin_path']) | ||
| mgr.freeze(args.freeze) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change "together with --use-uv" for something like "when uv indicated to use", aligned with argparse/man texts.