Skip to content

Commit

Permalink
Add --with-editable support to uv run (astral-sh#6262)
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb authored Aug 20, 2024
1 parent c3c05c4 commit f10ccc4
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 2 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 @@ -2140,6 +2140,14 @@ pub struct RunArgs {
#[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 project environment in a separate, ephemeral environment. These
/// dependencies are allowed to conflict with those specified by the project.
#[arg(long)]
pub with_editable: Vec<String>,

/// Run with all packages listed in the given `requirements.txt` files.
///
/// The same environment semantics as `--with` apply.
Expand Down
11 changes: 10 additions & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,16 @@ pub(crate) async fn run(
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
Err(ProjectError::Operation(operations::Error::Named(err))) => {
let err = miette::Report::msg(format!("{err}"))
.context("Invalid `--with` requirement");
eprint!("{err:?}");
return Ok(ExitStatus::Failure);
}

Err(err) => {
return Err(err.into());
}
};

environment.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 @@ -1011,6 +1011,11 @@ async fn run_project(
.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 @@ -193,6 +193,7 @@ pub(crate) struct RunSettings {
pub(crate) dev: bool,
pub(crate) command: ExternalCommand,
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 @@ -215,6 +216,7 @@ impl RunSettings {
no_dev,
command,
with,
with_editable,
with_requirements,
isolated,
locked,
Expand All @@ -238,6 +240,7 @@ impl RunSettings {
dev: flag(dev, no_dev).unwrap_or(true),
command,
with,
with_editable,
with_requirements: with_requirements
.into_iter()
.filter_map(Maybe::into_option)
Expand Down
137 changes: 136 additions & 1 deletion crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use indoc::indoc;

use uv_python::PYTHON_VERSION_FILENAME;

use common::{uv_snapshot, TestContext};
use common::{copy_dir_all, uv_snapshot, TestContext};

mod common;

Expand Down Expand Up @@ -538,6 +538,141 @@ fn run_with() -> Result<()> {
Ok(())
}

#[test]
fn run_with_editable() -> Result<()> {
let context = TestContext::new("3.12");

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
"
})?;

// Requesting an editable requirement should install it in a layer.
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/black_editable").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ foo==1.0.0 (from file://[TEMP_DIR]/)
+ idna==3.6
+ sniffio==1.3.1
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ black==0.1.0 (from file://[TEMP_DIR]/src/black_editable)
"###);

// Requesting an editable requirement should install it in a layer, even if it satisfied
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
"###);

// Requesting the project itself should use the base environment.
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg(".").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Audited 4 packages in [TIME]
"###);

// Similarly, an already editable requirement does not require a layer
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["anyio", "sniffio==1.3.1"]
[tool.uv.sources]
anyio = { path = "./src/anyio_local", editable = true }
"#
})?;

uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 3 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.3.0
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
~ foo==1.0.0 (from file://[TEMP_DIR]/)
- idna==3.6
"###);

uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Audited 3 packages in [TIME]
"###);

// If invalid, we should reference `--with-editable`.
uv_snapshot!(context.filters(), context.run().arg("--with").arg("./foo").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Audited 3 packages in [TIME]
× Invalid `--with` requirement
╰─▶ Distribution not found at: file://[TEMP_DIR]/foo
"###);

Ok(())
}

#[test]
fn run_locked() -> Result<()> {
let context = TestContext::new("3.12");
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ uv run [OPTIONS] <COMMAND>

<p>When used in a project, these dependencies will be layered on top of the project environment in a separate, ephemeral environment. These dependencies are allowed to conflict with those specified by the project.</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 project environment in a separate, ephemeral environment. These dependencies are allowed to conflict with those specified by the project.</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>

<p>The same environment semantics as <code>--with</code> apply.</p>
Expand Down

0 comments on commit f10ccc4

Please sign in to comment.