Skip to content

Commit e8547ea

Browse files
authored
Merge pull request #136 from sbidoul/uv-support
Add experimental support for the uv installer
2 parents a2ced36 + 5ebce07 commit e8547ea

File tree

7 files changed

+83
-12
lines changed

7 files changed

+83
-12
lines changed

README.rst

+6
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,12 @@ pip-df sync
341341
dependencies of the project. If not
342342
specified, ask confirmation.
343343
344+
--installer [default|env-pip|uv]
345+
The installer to use. For now the default is
346+
to use 'pip' that is installed in the target
347+
environment. To use 'uv', pip-deepfreeze must be
348+
installed with the 'uv' extra.
349+
344350
--help Show this message and exit.
345351
346352
pip-df tree

news/135.feature

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add experimental support for `uv <https://github.com/astral-sh/uv>`_ as the installation
2+
command. For now we still need `pip` to be installed in the target environment, to
3+
inspect its content. A new ``--installer`` option is available to select the installer
4+
to use.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies=[
3333
dynamic = ["version"]
3434

3535
[project.optional-dependencies]
36+
"uv" = ["uv"]
3637
"test" = ["pytest", "pytest-cov", "pytest-xdist", "virtualenv", "setuptools", "wheel"]
3738
"mypy" = ["mypy", "types-toml", "types-setuptools"]
3839

src/pip_deepfreeze/__main__.py

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import typer
77
from packaging.utils import canonicalize_name
88

9+
from .pip import Installer
910
from .pyproject_toml import load_pyproject_toml
1011
from .sanity import check_env
1112
from .sync import sync as sync_operation
@@ -68,6 +69,9 @@ def sync(
6869
"Can be specified multiple times."
6970
),
7071
),
72+
installer: Installer = typer.Option(
73+
"default",
74+
),
7175
) -> None:
7276
"""Install/update the environment to match the project requirements.
7377
@@ -85,6 +89,7 @@ def sync(
8589
uninstall_unneeded=uninstall_unneeded,
8690
project_root=ctx.obj.project_root,
8791
post_sync_commands=post_sync_commands,
92+
installer=installer,
8893
)
8994

9095

src/pip_deepfreeze/pip.py

+58-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import importlib.util
12
import json
3+
import os
24
import shlex
5+
import sys
6+
from enum import Enum
7+
from functools import lru_cache
38
from importlib.resources import path as resource_path
49
from pathlib import Path
510
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, TypedDict, cast
611

12+
import typer
713
from packaging.utils import NormalizedName
814
from packaging.version import Version
915

@@ -29,6 +35,7 @@
2935
check_output,
3036
get_temp_path_in_dir,
3137
log_debug,
38+
log_error,
3239
log_info,
3340
log_warning,
3441
normalize_req_line,
@@ -41,11 +48,53 @@ class PipInspectReport(TypedDict, total=False):
4148
environment: Dict[str, str]
4249

4350

51+
class Installer(str, Enum):
52+
default = "default"
53+
envpip = "env-pip"
54+
uv = "uv"
55+
56+
57+
@lru_cache
58+
def _has_uv() -> bool:
59+
return bool(importlib.util.find_spec("uv"))
60+
61+
62+
def _env_pip_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
63+
return [python, "-m", "pip", "install"], {}
64+
65+
66+
def _uv_install_cmd_and_env(python: str) -> Tuple[List[str], Dict[str, str]]:
67+
# TODO when https://github.com/astral-sh/uv/issues/1396 is implemented,
68+
# we will not need to return the VIRTUAL_ENV environment variable.
69+
return [sys.executable, "-m", "uv", "pip", "install"], {
70+
"VIRTUAL_ENV": str(Path(python).parent.parent)
71+
}
72+
73+
74+
def _install_cmd_and_env(
75+
installer: Installer, python: str
76+
) -> Tuple[List[str], Dict[str, str]]:
77+
if installer == Installer.default:
78+
installer = Installer.envpip
79+
if installer == Installer.envpip:
80+
return _env_pip_install_cmd_and_env(python)
81+
elif installer == Installer.uv:
82+
if not _has_uv():
83+
log_error(
84+
"The 'uv' installer was requested but it is not available. "
85+
"Please install pip-deepfreeze with the 'uv' extra to use it."
86+
)
87+
raise typer.Exit(1)
88+
return _uv_install_cmd_and_env(python)
89+
raise NotImplementedError(f"Installer {installer} is not implemented.")
90+
91+
4492
def pip_upgrade_project(
4593
python: str,
4694
constraints_filename: Path,
4795
project_root: Path,
4896
extras: Optional[Sequence[NormalizedName]] = None,
97+
installer: Installer = Installer.envpip,
4998
) -> None:
5099
"""Upgrade a project.
51100
@@ -117,15 +166,14 @@ def pip_upgrade_project(
117166
# 4. install project with constraints
118167
project_name = get_project_name(python, project_root)
119168
log_info(f"Installing/updating {project_name}")
120-
cmd = [
121-
python,
122-
"-m",
123-
"pip",
124-
"install",
125-
"-c",
126-
f"{constraints_without_editables_filename}",
127-
*editable_constraints,
128-
]
169+
cmd, env = _install_cmd_and_env(installer, python)
170+
cmd.extend(
171+
[
172+
"-c",
173+
f"{constraints_without_editables_filename}",
174+
*editable_constraints,
175+
]
176+
)
129177
cmd.append("-e")
130178
if extras:
131179
extras_str = ",".join(extras)
@@ -141,7 +189,7 @@ def pip_upgrade_project(
141189
log_debug(constraints)
142190
else:
143191
log_debug(f"with empty {constraints_without_editables_filename}.")
144-
check_call(cmd)
192+
check_call(cmd, os.environ.copy().update(env))
145193

146194

147195
def _pip_list__env_info_json(python: str) -> InstalledDistributions:

src/pip_deepfreeze/sync.py

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from packaging.utils import NormalizedName
66

77
from .pip import (
8+
Installer,
89
pip_fixup_vcs_direct_urls,
910
pip_freeze_dependencies_by_extra,
1011
pip_uninstall,
@@ -43,6 +44,7 @@ def sync(
4344
uninstall_unneeded: Optional[bool],
4445
project_root: Path,
4546
post_sync_commands: Sequence[str] = (),
47+
installer: Installer = Installer.envpip,
4648
) -> None:
4749
project_name = get_project_name(python, project_root)
4850
project_name_with_extras = make_project_name_with_extras(project_name, extras)
@@ -64,6 +66,7 @@ def sync(
6466
constraints_path,
6567
project_root,
6668
extras=extras,
69+
installer=installer,
6770
)
6871
# freeze dependencies
6972
frozen_reqs_by_extra, unneeded_reqs = pip_freeze_dependencies_by_extra(

src/pip_deepfreeze/utils.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,13 @@ def log_error(msg: str) -> None:
8181
typer.secho(msg, fg=typer.colors.RED, err=True)
8282

8383

84-
def check_call(cmd: Sequence[Union[str, Path]], cwd: Optional[Path] = None) -> int:
84+
def check_call(
85+
cmd: Sequence[Union[str, Path]],
86+
cwd: Optional[Path] = None,
87+
env: Optional[Dict[str, str]] = None,
88+
) -> int:
8589
try:
86-
return subprocess.check_call(cmd, cwd=cwd)
90+
return subprocess.check_call(cmd, cwd=cwd, env=env)
8791
except CalledProcessError as e:
8892
cmd_str = shlex.join(str(item) for item in cmd)
8993
log_error(f"Error running: {cmd_str}.")

0 commit comments

Comments
 (0)