Skip to content

Commit d82d6fd

Browse files
authored
Use uv by default when pip installing (#215)
1 parent 6978d27 commit d82d6fd

File tree

5 files changed

+98
-22
lines changed

5 files changed

+98
-22
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This often leads to confusion and inefficiency, as developers juggle between mul
1818
- **📝 Unified Dependency File**: Use either `requirements.yaml` or `pyproject.toml` to manage both Conda and Pip dependencies in one place.
1919
- **⚙️ Build System Integration**: Integrates with Setuptools and Hatchling for automatic dependency handling during `pip install ./your-package`.
2020
- **💻 One-Command Installation**: `unidep install` handles Conda, Pip, and local dependencies effortlessly.
21+
- **⚡️ Fast Pip Operations**: Leverages `uv` (if installed) for faster pip installations.
2122
- **🏢 Monorepo-Friendly**: Render (multiple) `requirements.yaml` or `pyproject.toml` files into one Conda `environment.yaml` file and maintain fully consistent global *and* per sub package `conda-lock` files.
2223
- **🌍 Platform-Specific Support**: Specify dependencies for different operating systems or architectures.
2324
- **🔧 `pip-compile` Integration**: Generate fully pinned `requirements.txt` files from `requirements.yaml` or `pyproject.toml` files using `pip-compile`.
@@ -557,6 +558,7 @@ usage: unidep install [-h] [-v] [-e] [--skip-local] [--skip-pip]
557558
[--conda-env-name CONDA_ENV_NAME | --conda-env-prefix CONDA_ENV_PREFIX]
558559
[--dry-run] [--ignore-pin IGNORE_PIN]
559560
[--overwrite-pin OVERWRITE_PIN] [-f CONDA_LOCK_FILE]
561+
[--no-uv]
560562
files [files ...]
561563

562564
Automatically install all dependencies from one or more `requirements.yaml` or
@@ -591,7 +593,8 @@ options:
591593
specifying a different package to skip. For example,
592594
use `--skip-dependency pandas` to skip installing
593595
pandas.
594-
--no-dependencies Skip installing dependencies from `requirements.yaml`
596+
--no-dependencies, --no-deps
597+
Skip installing dependencies from `requirements.yaml`
595598
or `pyproject.toml` file(s) and only install local
596599
package(s). Useful after installing a `conda-lock.yml`
597600
file because then all dependencies have already been
@@ -621,6 +624,8 @@ options:
621624
the new environment. Assumes that the lock file
622625
contains all dependencies. Must be used with `--conda-
623626
env-name` or `--conda-env-prefix`.
627+
--no-uv Disables the use of `uv` for pip install. By default,
628+
`uv` is used if it is available in the PATH.
624629
```
625630
626631
<!-- OUTPUT:END -->
@@ -645,6 +650,7 @@ usage: unidep install [-h] [-v] [-e] [--skip-local] [--skip-pip]
645650
[--conda-env-name CONDA_ENV_NAME | --conda-env-prefix CONDA_ENV_PREFIX]
646651
[--dry-run] [--ignore-pin IGNORE_PIN]
647652
[--overwrite-pin OVERWRITE_PIN] [-f CONDA_LOCK_FILE]
653+
[--no-uv]
648654
files [files ...]
649655

650656
Automatically install all dependencies from one or more `requirements.yaml` or
@@ -679,7 +685,8 @@ options:
679685
specifying a different package to skip. For example,
680686
use `--skip-dependency pandas` to skip installing
681687
pandas.
682-
--no-dependencies Skip installing dependencies from `requirements.yaml`
688+
--no-dependencies, --no-deps
689+
Skip installing dependencies from `requirements.yaml`
683690
or `pyproject.toml` file(s) and only install local
684691
package(s). Useful after installing a `conda-lock.yml`
685692
file because then all dependencies have already been
@@ -709,6 +716,8 @@ options:
709716
the new environment. Assumes that the lock file
710717
contains all dependencies. Must be used with `--conda-
711718
env-name` or `--conda-env-prefix`.
719+
--no-uv Disables the use of `uv` for pip install. By default,
720+
`uv` is used if it is available in the PATH.
712721
```
713722
714723
<!-- OUTPUT:END -->

example/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ $ unidep install --dry-run -e ./setup_py_project
114114

115115
📝 Found local dependencies: {'setup_py_project': ['hatch_project', 'setuptools_project']}
116116

117-
📦 Installing project with `/opt/hostedtoolcache/Python/3.12.8/x64/bin/python -m pip install --no-dependencies -e /home/runner/work/unidep/unidep/example/hatch_project -e /home/runner/work/unidep/unidep/example/setuptools_project -e ./setup_py_project`
117+
📦 Installing project with `/opt/hostedtoolcache/Python/3.12.8/x64/bin/python -m pip install --no-deps -e /home/runner/work/unidep/unidep/example/hatch_project -e /home/runner/work/unidep/unidep/example/setuptools_project -e ./setup_py_project`
118118

119119
```
120120

@@ -161,7 +161,7 @@ $ unidep install-all -e --dry-run
161161

162162
📝 Found local dependencies: {'pyproject_toml_project': ['hatch_project'], 'setup_py_project': ['hatch_project', 'setuptools_project'], 'setuptools_project': ['hatch_project']}
163163

164-
📦 Installing project with `/opt/hostedtoolcache/Python/3.12.8/x64/bin/python -m pip install --no-dependencies -e ./hatch2_project -e ./hatch_project -e ./pyproject_toml_project -e ./setup_py_project -e ./setuptools_project`
164+
📦 Installing project with `/opt/hostedtoolcache/Python/3.12.8/x64/bin/python -m pip install --no-deps -e ./hatch2_project -e ./hatch_project -e ./pyproject_toml_project -e ./setup_py_project -e ./setuptools_project`
165165

166166
```
167167

tests/test_cli.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import os
6+
import platform
67
import re
78
import shutil
89
import subprocess
@@ -127,10 +128,26 @@ def test_install_all_command(capsys: pytest.CaptureFixture) -> None:
127128
assert "Installing pip dependencies" in captured.out
128129
projects = [REPO_ROOT / "example" / p for p in EXAMPLE_PROJECTS]
129130
pkgs = " ".join([f"-e {p}" for p in sorted(projects)])
130-
assert f"pip install --no-dependencies {pkgs}`" in captured.out
131+
assert f"pip install --no-deps {pkgs}`" in captured.out
131132

132133

133-
def test_unidep_install_all_dry_run() -> None:
134+
def mock_uv_env(tmp_path: Path) -> dict[str, str]:
135+
"""Create a mock uv executable and return env with it in the PATH."""
136+
mock_uv_path = tmp_path / ("uv.bat" if platform.system() == "Windows" else "uv")
137+
if platform.system() == "Windows":
138+
mock_uv_path.write_text("@echo off\necho Mock uv called %*")
139+
else:
140+
mock_uv_path.write_text("#!/bin/sh\necho 'Mock uv called' \"$@\"")
141+
mock_uv_path.chmod(0o755) # Make it executable
142+
143+
# Add tmp_path to the PATH environment variable
144+
env = os.environ.copy()
145+
env["PATH"] = f"{tmp_path}{os.pathsep}{env['PATH']}"
146+
return env
147+
148+
149+
@pytest.mark.parametrize("with_uv", [True, False])
150+
def test_unidep_install_all_dry_run(tmp_path: Path, with_uv: bool) -> None: # noqa: FBT001
134151
# Path to the requirements file
135152
requirements_path = REPO_ROOT / "example"
136153

@@ -146,11 +163,13 @@ def test_unidep_install_all_dry_run() -> None:
146163
"--editable",
147164
"--directory",
148165
str(requirements_path),
166+
*(["--no-uv"] if not with_uv else []),
149167
],
150168
check=True,
151169
capture_output=True,
152170
text=True,
153171
encoding="utf-8",
172+
env=mock_uv_env(tmp_path) if with_uv else None,
154173
)
155174

156175
# Check the output
@@ -165,7 +184,10 @@ def test_unidep_install_all_dry_run() -> None:
165184
projects = [REPO_ROOT / "example" / p for p in EXAMPLE_PROJECTS]
166185
pkgs = " ".join([f"-e {p}" for p in sorted(projects)])
167186
assert "📦 Installing project with `" in result.stdout
168-
assert f" -m pip install --no-dependencies {pkgs}" in result.stdout
187+
if with_uv:
188+
assert "uv pip install --python" in result.stdout
189+
else:
190+
assert f" -m pip install --no-deps {pkgs}" in result.stdout
169191

170192

171193
def test_unidep_conda() -> None:
@@ -256,6 +278,7 @@ def test_doubly_nested_project_folder_installable(
256278
"--dry-run",
257279
"--editable",
258280
"--no-dependencies",
281+
"--no-uv",
259282
str(project4 / "requirements.yaml"),
260283
],
261284
check=True,
@@ -269,7 +292,7 @@ def test_doubly_nested_project_folder_installable(
269292
p3 = str(tmp_path / "example" / "setuptools_project")
270293
p4 = str(tmp_path / "example" / "extra_projects" / "project4")
271294
pkgs = " ".join([f"-e {p}" for p in sorted((p1, p2, p3, p4))])
272-
assert f"pip install --no-dependencies {pkgs}`" in result.stdout
295+
assert f"pip install --no-deps {pkgs}`" in result.stdout
273296

274297
p5 = str(tmp_path / "example" / "pyproject_toml_project")
275298
p6 = str(tmp_path / "example" / "hatch2_project")
@@ -281,6 +304,7 @@ def test_doubly_nested_project_folder_installable(
281304
"--dry-run",
282305
"--editable",
283306
"--no-dependencies",
307+
"--no-uv",
284308
"--directory",
285309
str(example_folder),
286310
"--depth",
@@ -292,7 +316,7 @@ def test_doubly_nested_project_folder_installable(
292316
encoding="utf-8",
293317
)
294318
pkgs = " ".join([f"-e {p}" for p in sorted((p1, p2, p3, p4, p5, p6))])
295-
assert f"pip install --no-dependencies {pkgs}`" in result.stdout
319+
assert f"pip install --no-deps {pkgs}`" in result.stdout
296320

297321
# Test depth 1 (should not install project4)
298322
result = subprocess.run(
@@ -302,6 +326,7 @@ def test_doubly_nested_project_folder_installable(
302326
"--dry-run",
303327
"--editable",
304328
"--no-dependencies",
329+
"--no-uv",
305330
"--directory",
306331
str(example_folder),
307332
"--depth",
@@ -313,7 +338,7 @@ def test_doubly_nested_project_folder_installable(
313338
encoding="utf-8",
314339
)
315340
pkgs = " ".join([f"-e {p}" for p in sorted((p1, p2, p3, p5, p6))])
316-
assert f"pip install --no-dependencies {pkgs}`" in result.stdout
341+
assert f"pip install --no-deps {pkgs}`" in result.stdout
317342

318343

319344
def test_pip_compile_command(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:

unidep/_cli.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def _add_common_args( # noqa: PLR0912, C901
177177
if "no-dependencies" in options:
178178
sub_parser.add_argument(
179179
"--no-dependencies",
180+
"--no-deps",
180181
action="store_true",
181182
help=f"Skip installing dependencies from {_DEP_FILES}"
182183
" file(s) and only install local package(s). Useful after"
@@ -245,6 +246,13 @@ def _add_common_args( # noqa: PLR0912, C901
245246
" environment. Assumes that the lock file contains all dependencies."
246247
" Must be used with `--conda-env-name` or `--conda-env-prefix`.",
247248
)
249+
if "no-uv" in options:
250+
sub_parser.add_argument(
251+
"--no-uv",
252+
action="store_true",
253+
help="Disables the use of `uv` for pip install. By default, `uv` is used"
254+
" if it is available in the PATH.",
255+
)
248256

249257

250258
def _add_extra_flags(
@@ -373,6 +381,7 @@ def _parse_args() -> argparse.Namespace:
373381
"ignore-pin",
374382
"skip-dependency",
375383
"overwrite-pin",
384+
"no-uv",
376385
"verbose",
377386
},
378387
)
@@ -416,6 +425,7 @@ def _parse_args() -> argparse.Namespace:
416425
"ignore-pin",
417426
"skip-dependency",
418427
"overwrite-pin",
428+
"no-uv",
419429
"verbose",
420430
},
421431
)
@@ -833,15 +843,27 @@ def _python_executable(
833843
return str(python_executable)
834844

835845

846+
def _use_uv(no_uv: bool) -> bool: # noqa: FBT001
847+
"""Check if the user wants to use the `uv` package."""
848+
if no_uv:
849+
return False
850+
return shutil.which("uv") is not None
851+
852+
836853
def _pip_install_local(
837854
*folders: str | Path,
838855
editable: bool,
839856
dry_run: bool,
840857
python_executable: str,
841858
conda_run: list[str],
859+
no_uv: bool,
842860
flags: list[str] | None = None,
843861
) -> None: # pragma: no cover
844-
pip_command = [*conda_run, python_executable, "-m", "pip", "install"]
862+
if _use_uv(no_uv):
863+
pip_command = ["uv", "pip", "install", "--python", python_executable]
864+
else:
865+
pip_command = [*conda_run, python_executable, "-m", "pip", "install"]
866+
845867
if flags:
846868
pip_command.extend(flags)
847869

@@ -879,6 +901,7 @@ def _install_command( # noqa: PLR0912, PLR0915
879901
ignore_pins: list[str] | None = None,
880902
overwrite_pins: list[str] | None = None,
881903
skip_dependencies: list[str] | None = None,
904+
no_uv: bool = True,
882905
verbose: bool = False,
883906
) -> None:
884907
"""Install the dependencies of a single `requirements.yaml` or `pyproject.toml` file.""" # noqa: E501
@@ -952,15 +975,29 @@ def _install_command( # noqa: PLR0912, PLR0915
952975
conda_env_prefix,
953976
)
954977
if env_spec.pip and not skip_pip:
955-
conda_run = _maybe_conda_run(conda_executable, conda_env_name, conda_env_prefix)
956-
pip_command = [
957-
*conda_run,
958-
python_executable,
959-
"-m",
960-
"pip",
961-
"install",
962-
*env_spec.pip,
963-
]
978+
if _use_uv(no_uv):
979+
pip_command = [
980+
"uv",
981+
"pip",
982+
"install",
983+
"--python",
984+
python_executable,
985+
*env_spec.pip,
986+
]
987+
else:
988+
conda_run = _maybe_conda_run(
989+
conda_executable,
990+
conda_env_name,
991+
conda_env_prefix,
992+
)
993+
pip_command = [
994+
*conda_run,
995+
python_executable,
996+
"-m",
997+
"pip",
998+
"install",
999+
*env_spec.pip,
1000+
]
9641001
print(f"📦 Installing pip dependencies with `{' '.join(pip_command)}`\n")
9651002
if not dry_run: # pragma: no cover
9661003
subprocess.run(pip_command, check=True)
@@ -992,7 +1029,7 @@ def _install_command( # noqa: PLR0912, PLR0915
9921029
if dep.resolve() not in installable_set
9931030
]
9941031
if installable:
995-
pip_flags = ["--no-dependencies"] # we just ran pip/conda install, so skip
1032+
pip_flags = ["--no-deps"] # we just ran pip/conda install, so skip
9961033
if verbose:
9971034
pip_flags.append("--verbose")
9981035
conda_run = _maybe_conda_run(
@@ -1006,6 +1043,7 @@ def _install_command( # noqa: PLR0912, PLR0915
10061043
dry_run=dry_run,
10071044
python_executable=python_executable,
10081045
flags=pip_flags,
1046+
no_uv=no_uv,
10091047
conda_run=conda_run,
10101048
)
10111049

@@ -1030,6 +1068,7 @@ def _install_all_command(
10301068
ignore_pins: list[str] | None = None,
10311069
overwrite_pins: list[str] | None = None,
10321070
skip_dependencies: list[str] | None = None,
1071+
no_uv: bool = True,
10331072
verbose: bool = False,
10341073
) -> None: # pragma: no cover
10351074
found_files = find_requirements_files(
@@ -1055,6 +1094,7 @@ def _install_all_command(
10551094
ignore_pins=ignore_pins,
10561095
overwrite_pins=overwrite_pins,
10571096
skip_dependencies=skip_dependencies,
1097+
no_uv=no_uv,
10581098
verbose=verbose,
10591099
)
10601100

@@ -1471,6 +1511,7 @@ def main() -> None:
14711511
ignore_pins=args.ignore_pin,
14721512
skip_dependencies=args.skip_dependency,
14731513
overwrite_pins=args.overwrite_pin,
1514+
no_uv=args.no_uv,
14741515
verbose=args.verbose,
14751516
)
14761517
elif args.command == "install-all":
@@ -1492,6 +1533,7 @@ def main() -> None:
14921533
ignore_pins=args.ignore_pin,
14931534
skip_dependencies=args.skip_dependency,
14941535
overwrite_pins=args.overwrite_pin,
1536+
no_uv=args.no_uv,
14951537
verbose=args.verbose,
14961538
)
14971539
elif args.command == "conda-lock": # pragma: no cover

unidep/_dependencies_parsing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ def _extract_local_dependencies( # noqa: PLR0912
624624
f"⚠️ Installing a local dependency (`{abs_local.name}`) which"
625625
" is not managed by unidep, this will skip all of its"
626626
" dependencies, i.e., it will call `pip install` with"
627-
" `--no-dependencies`. To properly manage this dependency,"
627+
" `--no-deps`. To properly manage this dependency,"
628628
" add a `requirements.yaml` or `pyproject.toml` file with"
629629
" `[tool.unidep]` in its directory.",
630630
)

0 commit comments

Comments
 (0)