Add support for --with-editable to uv tool (#6744)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> close #6272 ## Test Plan <!-- How was it tested? --> As in https://github.com/astral-sh/uv/pull/6262 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
@@ -3204,6 +3204,14 @@ pub struct ToolRunArgs {
|
||||
#[arg(long)]
|
||||
pub with: Vec<String>,
|
||||
|
||||
/// Run with the given packages installed as editables
|
||||
///
|
||||
/// When used in a project, these dependencies will be layered on top of
|
||||
/// the uv tool's environment in a separate, ephemeral environment. These
|
||||
/// dependencies are allowed to conflict with those specified.
|
||||
#[arg(long)]
|
||||
pub with_editable: Vec<String>,
|
||||
|
||||
/// Run with all packages listed in the given `requirements.txt` files.
|
||||
#[arg(long, value_parser = parse_maybe_file_path)]
|
||||
pub with_requirements: Vec<Maybe<PathBuf>>,
|
||||
|
||||
@@ -22,7 +22,9 @@ use uv_python::{
|
||||
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVersionFile, VersionRequest,
|
||||
};
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
|
||||
use uv_requirements::{
|
||||
NamedRequirementsError, NamedRequirementsResolver, RequirementsSpecification,
|
||||
};
|
||||
use uv_resolver::{
|
||||
FlatIndex, OptionsBuilder, PythonRequirement, RequiresPython, ResolutionGraph, ResolverMarkers,
|
||||
};
|
||||
@@ -548,7 +550,7 @@ pub(crate) async fn resolve_names(
|
||||
native_tls: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<Vec<Requirement>> {
|
||||
) -> Result<Vec<Requirement>, NamedRequirementsError> {
|
||||
// Partition the requirements into named and unnamed requirements.
|
||||
let (mut requirements, unnamed): (Vec<_>, Vec<_>) =
|
||||
requirements
|
||||
|
||||
@@ -663,10 +663,7 @@ pub(crate) async fn run(
|
||||
eprint!("{err:?}");
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
environment.into()
|
||||
|
||||
@@ -130,6 +130,11 @@ pub(crate) async fn run(
|
||||
eprint!("{report:?}");
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(ProjectError::NamedRequirements(err)) => {
|
||||
let err = miette::Report::msg(format!("{err}")).context("Invalid `--with` requirement");
|
||||
eprint!("{err:?}");
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
|
||||
@@ -828,6 +828,11 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
.with
|
||||
.into_iter()
|
||||
.map(RequirementsSource::from_package)
|
||||
.chain(
|
||||
args.with_editable
|
||||
.into_iter()
|
||||
.map(RequirementsSource::Editable),
|
||||
)
|
||||
.chain(
|
||||
args.with_requirements
|
||||
.into_iter()
|
||||
|
||||
@@ -290,6 +290,7 @@ pub(crate) struct ToolRunSettings {
|
||||
pub(crate) command: Option<ExternalCommand>,
|
||||
pub(crate) from: Option<String>,
|
||||
pub(crate) with: Vec<String>,
|
||||
pub(crate) with_editable: Vec<String>,
|
||||
pub(crate) with_requirements: Vec<PathBuf>,
|
||||
pub(crate) isolated: bool,
|
||||
pub(crate) show_resolution: bool,
|
||||
@@ -310,6 +311,7 @@ impl ToolRunSettings {
|
||||
command,
|
||||
from,
|
||||
with,
|
||||
with_editable,
|
||||
with_requirements,
|
||||
isolated,
|
||||
show_resolution,
|
||||
@@ -341,6 +343,7 @@ impl ToolRunSettings {
|
||||
command,
|
||||
from,
|
||||
with,
|
||||
with_editable,
|
||||
with_requirements: with_requirements
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
|
||||
@@ -810,7 +810,7 @@ fn run_with_editable() -> Result<()> {
|
||||
"###);
|
||||
|
||||
// If invalid, we should reference `--with-editable`.
|
||||
uv_snapshot!(context.filters(), context.run().arg("--with").arg("./foo").arg("main.py"), @r###"
|
||||
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./foo").arg("main.py"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
+140
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_fs::prelude::*;
|
||||
use common::{uv_snapshot, TestContext};
|
||||
use common::{copy_dir_all, uv_snapshot, TestContext};
|
||||
use indoc::indoc;
|
||||
|
||||
mod common;
|
||||
@@ -823,6 +823,145 @@ fn tool_run_without_output() {
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_run_with_editable() -> anyhow::Result<()> {
|
||||
let context = TestContext::new("3.12").with_filtered_counts();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let anyio_local = context.temp_dir.child("src").child("anyio_local");
|
||||
copy_dir_all(
|
||||
context.workspace_root.join("scripts/packages/anyio_local"),
|
||||
&anyio_local,
|
||||
)?;
|
||||
|
||||
let black_editable = context.temp_dir.child("src").child("black_editable");
|
||||
copy_dir_all(
|
||||
context
|
||||
.workspace_root
|
||||
.join("scripts/packages/black_editable"),
|
||||
&black_editable,
|
||||
)?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! { r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio", "sniffio==1.3.1"]
|
||||
"#
|
||||
})?;
|
||||
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc! { r"
|
||||
import sniffio
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--with-editable")
|
||||
.arg("./src/black_editable")
|
||||
.arg("--with")
|
||||
.arg("iniconfig")
|
||||
.arg("flask")
|
||||
.arg("--version")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Python 3.12.[X]
|
||||
Flask 3.0.2
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ black==0.1.0 (from file://[TEMP_DIR]/src/black_editable)
|
||||
+ blinker==1.7.0
|
||||
+ click==8.1.7
|
||||
+ flask==3.0.2
|
||||
+ iniconfig==2.0.0
|
||||
+ itsdangerous==2.1.2
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
+ werkzeug==3.0.1
|
||||
"###);
|
||||
|
||||
// Requesting an editable requirement should install it in a layer, even if it satisfied
|
||||
uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg("./src/anyio_local").arg("flask").arg("--version").env("UV_TOOL_DIR", tool_dir.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
|
||||
, @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Python 3.12.[X]
|
||||
Flask 3.0.2
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
|
||||
+ blinker==1.7.0
|
||||
+ click==8.1.7
|
||||
+ flask==3.0.2
|
||||
+ itsdangerous==2.1.2
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
+ werkzeug==3.0.1
|
||||
"###);
|
||||
|
||||
// Requesting the project itself should use a new environment.
|
||||
uv_snapshot!(context.filters(), context.tool_run().arg("--with-editable").arg(".").arg("flask").arg("--version").env("UV_TOOL_DIR", tool_dir.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Python 3.12.[X]
|
||||
Flask 3.0.2
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ blinker==1.7.0
|
||||
+ click==8.1.7
|
||||
+ flask==3.0.2
|
||||
+ foo==1.0.0 (from file://[TEMP_DIR]/)
|
||||
+ idna==3.6
|
||||
+ itsdangerous==2.1.2
|
||||
+ jinja2==3.1.3
|
||||
+ markupsafe==2.1.5
|
||||
+ sniffio==1.3.1
|
||||
+ werkzeug==3.0.1
|
||||
"###);
|
||||
|
||||
// If invalid, we should reference `--with`.
|
||||
uv_snapshot!(context.filters(), context
|
||||
.tool_run()
|
||||
.arg("--with")
|
||||
.arg("./foo")
|
||||
.arg("flask")
|
||||
.arg("--version")
|
||||
.env("UV_TOOL_DIR", tool_dir
|
||||
.as_os_str()).env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× Invalid `--with` requirement
|
||||
╰─▶ Distribution not found at: file://[TEMP_DIR]/foo
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warn_no_executables_found() {
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
|
||||
@@ -2593,6 +2593,10 @@ uv tool run [OPTIONS] [COMMAND]
|
||||
|
||||
</dd><dt><code>--with</code> <i>with</i></dt><dd><p>Run with the given packages installed</p>
|
||||
|
||||
</dd><dt><code>--with-editable</code> <i>with-editable</i></dt><dd><p>Run with the given packages installed as editables</p>
|
||||
|
||||
<p>When used in a project, these dependencies will be layered on top of the uv tool’s environment in a separate, ephemeral environment. These dependencies are allowed to conflict with those specified.</p>
|
||||
|
||||
</dd><dt><code>--with-requirements</code> <i>with-requirements</i></dt><dd><p>Run with all packages listed in the given <code>requirements.txt</code> files</p>
|
||||
|
||||
</dd></dl>
|
||||
|
||||
Reference in New Issue
Block a user