Skip to content

Commit

Permalink
Add support for --with-editable to uv tool (#6744)
Browse files Browse the repository at this point in the history
<!--
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 #6262

---------

Co-authored-by: Zanie Blue <[email protected]>
  • Loading branch information
kyoto7250 and zanieb authored Sep 17, 2024
1 parent 6c52f36 commit e5dd67f
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 8 deletions.
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand Down
6 changes: 4 additions & 2 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
};

Expand Down
5 changes: 5 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -310,6 +311,7 @@ impl ToolRunSettings {
command,
from,
with,
with_editable,
with_requirements,
isolated,
show_resolution,
Expand Down Expand Up @@ -341,6 +343,7 @@ impl ToolRunSettings {
command,
from,
with,
with_editable,
with_requirements: with_requirements
.into_iter()
.filter_map(Maybe::into_option)
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand Down
141 changes: 140 additions & 1 deletion crates/uv/tests/tool_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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&#8217;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>
Expand Down

0 comments on commit e5dd67f

Please sign in to comment.