mirror of https://github.com/astral-sh/uv
Editable installs for `uv tool` (#5454)
## Summary Resolves #5436. ## Test Plan `cargo test` ```console ❯ ./target/debug/uv tool install -e ~/black warning: `uv tool install` is experimental and may change without warning Resolved 6 packages in 894ms Built black @ file:///Users/ahmedilyas/black Prepared 1 package in 468ms Installed 6 packages in 6ms + black==24.4.3.dev23+g7e2afc9 (from file:///Users/ahmedilyas/black) + click==8.1.7 + mypy-extensions==1.0.0 + packaging==24.1 + pathspec==0.12.1 + platformdirs==4.2.2 Installed 2 executables: black, blackd ``` venv has the `.pth` files. ```console ❯ eza /Users/ahmedilyas/Library/Application\ Support/uv/tools/black/lib/python3.12/site-packages/ _black.pth _virtualenv.py click mypy_extensions-1.0.0.dist-info packaging pathspec platformdirs _virtualenv.pth black-24.4.3.dev23+g7e2afc9.dist-info click-8.1.7.dist-info mypy_extensions.py packaging-24.1.dist-info pathspec-0.12.1.dist-info platformdirs-4.2.2.dist-info ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
2186e967f6
commit
e8d7c0cb58
|
|
@ -2283,6 +2283,9 @@ pub struct ToolInstallArgs {
|
|||
/// The package to install commands from.
|
||||
pub package: String,
|
||||
|
||||
#[arg(short, long)]
|
||||
pub editable: bool,
|
||||
|
||||
/// The package to install commands from.
|
||||
///
|
||||
/// This option is provided for parity with `uv tool run`, but is redundant with `package`.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ use crate::settings::ResolverInstallerSettings;
|
|||
/// Install a tool.
|
||||
pub(crate) async fn install(
|
||||
package: String,
|
||||
editable: bool,
|
||||
from: Option<String>,
|
||||
with: &[RequirementsSource],
|
||||
python: Option<String>,
|
||||
|
|
@ -82,6 +83,9 @@ pub(crate) async fn install(
|
|||
|
||||
// Initialize any shared state.
|
||||
let state = SharedState::default();
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls);
|
||||
|
||||
// Resolve the `from` requirement.
|
||||
let from = if let Some(from) = from {
|
||||
|
|
@ -91,9 +95,18 @@ pub(crate) async fn install(
|
|||
bail!("Package requirement (`{from}`) provided with `--from` conflicts with install request (`{package}`)", from = from.cyan(), package = package.cyan())
|
||||
};
|
||||
|
||||
let source = if editable {
|
||||
RequirementsSource::Editable(from)
|
||||
} else {
|
||||
RequirementsSource::Package(from)
|
||||
};
|
||||
let requirements = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
.await?
|
||||
.requirements;
|
||||
|
||||
let from_requirement = {
|
||||
resolve_names(
|
||||
vec![RequirementsSpecification::parse_package(&from)?],
|
||||
requirements,
|
||||
&interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
|
|
@ -121,8 +134,17 @@ pub(crate) async fn install(
|
|||
|
||||
from_requirement
|
||||
} else {
|
||||
let source = if editable {
|
||||
RequirementsSource::Editable(package.clone())
|
||||
} else {
|
||||
RequirementsSource::Package(package.clone())
|
||||
};
|
||||
let requirements = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
.await?
|
||||
.requirements;
|
||||
|
||||
resolve_names(
|
||||
vec![RequirementsSpecification::parse_package(&package)?],
|
||||
requirements,
|
||||
&interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
|
|
@ -139,12 +161,7 @@ pub(crate) async fn install(
|
|||
};
|
||||
|
||||
// Read the `--with` requirements.
|
||||
let spec = {
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls);
|
||||
RequirementsSpecification::from_simple_sources(with, &client_builder).await?
|
||||
};
|
||||
let spec = RequirementsSpecification::from_simple_sources(with, &client_builder).await?;
|
||||
|
||||
// Resolve the `--from` and `--with` requirements.
|
||||
let requirements = {
|
||||
|
|
|
|||
|
|
@ -681,6 +681,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
|
||||
commands::tool_install(
|
||||
args.package,
|
||||
args.editable,
|
||||
args.from,
|
||||
&requirements,
|
||||
args.python,
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ pub(crate) struct ToolInstallSettings {
|
|||
pub(crate) refresh: Refresh,
|
||||
pub(crate) settings: ResolverInstallerSettings,
|
||||
pub(crate) force: bool,
|
||||
pub(crate) editable: bool,
|
||||
}
|
||||
|
||||
impl ToolInstallSettings {
|
||||
|
|
@ -310,6 +311,7 @@ impl ToolInstallSettings {
|
|||
pub(crate) fn resolve(args: ToolInstallArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let ToolInstallArgs {
|
||||
package,
|
||||
editable,
|
||||
from,
|
||||
with,
|
||||
with_requirements,
|
||||
|
|
@ -330,6 +332,7 @@ impl ToolInstallSettings {
|
|||
.collect(),
|
||||
python,
|
||||
force,
|
||||
editable,
|
||||
refresh: Refresh::from(refresh),
|
||||
settings: ResolverInstallerSettings::combine(
|
||||
resolver_installer_options(installer, build),
|
||||
|
|
|
|||
|
|
@ -319,6 +319,236 @@ fn tool_install_version() {
|
|||
"###);
|
||||
}
|
||||
|
||||
/// Test an editable installation of a tool.
|
||||
#[test]
|
||||
fn tool_install_editable() {
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `black` as an editable package.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("-e")
|
||||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||||
Installed 1 executable: black
|
||||
"###);
|
||||
|
||||
tool_dir.child("black").assert(predicate::path::is_dir());
|
||||
tool_dir
|
||||
.child("black")
|
||||
.child("uv-receipt.toml")
|
||||
.assert(predicate::path::exists());
|
||||
|
||||
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||
assert!(executable.exists());
|
||||
|
||||
// On Windows, we can't snapshot an executable file.
|
||||
#[cfg(not(windows))]
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// Should run black in the virtual environment
|
||||
assert_snapshot!(fs_err::read_to_string(&executable).unwrap(), @r###"
|
||||
#![TEMP_DIR]/tools/black/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from black import main
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
"###);
|
||||
|
||||
});
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||
[tool]
|
||||
requirements = ["black @ file://[WORKSPACE]/scripts/packages/black_editable"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
]
|
||||
"###);
|
||||
});
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello world!
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// Request `black`. It should retain the current installation.
|
||||
// TODO(charlie): This is arguably incorrect, especially because the tool receipt removes the
|
||||
// file path.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Installed 1 executable: black
|
||||
"###);
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||
[tool]
|
||||
requirements = ["black"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
]
|
||||
"###);
|
||||
});
|
||||
|
||||
// Request `black` at a different version. It should install a new version.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black")
|
||||
.arg("--from")
|
||||
.arg("black==24.2.0")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 6 packages in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
- black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||||
+ black==24.2.0
|
||||
+ click==8.1.7
|
||||
+ mypy-extensions==1.0.0
|
||||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
Installed 2 executables: black, blackd
|
||||
"###);
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||
[tool]
|
||||
requirements = ["black==24.2.0"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||
]
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
/// Test an editable installation of a tool using `--from`.
|
||||
#[test]
|
||||
fn tool_install_editable_from() {
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `black` as an editable package.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black")
|
||||
.arg("-e")
|
||||
.arg("--from")
|
||||
.arg(context.workspace_root.join("scripts/packages/black_editable"))
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
|
||||
Installed 1 executable: black
|
||||
"###);
|
||||
|
||||
tool_dir.child("black").assert(predicate::path::is_dir());
|
||||
tool_dir
|
||||
.child("black")
|
||||
.child("uv-receipt.toml")
|
||||
.assert(predicate::path::exists());
|
||||
|
||||
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||
assert!(executable.exists());
|
||||
|
||||
// On Windows, we can't snapshot an executable file.
|
||||
#[cfg(not(windows))]
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// Should run black in the virtual environment
|
||||
assert_snapshot!(fs_err::read_to_string(&executable).unwrap(), @r###"
|
||||
#![TEMP_DIR]/tools/black/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from black import main
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
||||
"###);
|
||||
|
||||
});
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||
[tool]
|
||||
requirements = ["black @ file://[WORKSPACE]/scripts/packages/black_editable"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
]
|
||||
"###);
|
||||
});
|
||||
|
||||
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Hello world!
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
/// Test installing a tool with `uv tool install --from`
|
||||
#[test]
|
||||
fn tool_install_from() {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
def a():
|
||||
pass
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ dependencies = []
|
|||
requires-python = ">=3.11,<3.13"
|
||||
license = {text = "MIT"}
|
||||
|
||||
[project.scripts]
|
||||
black = "black:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
colorama = ["colorama>=0.4.3"]
|
||||
uvloop = ["uvloop>=0.15.2"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue