Skip to content

Commit 70a624b

Browse files
committed
Add --with-editable support to uv run
1 parent e740322 commit 70a624b

File tree

6 files changed

+166
-2
lines changed

6 files changed

+166
-2
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,14 @@ pub struct RunArgs {
21402140
#[arg(long)]
21412141
pub with: Vec<String>,
21422142

2143+
/// Run with the given packages installed as editables
2144+
///
2145+
/// When used in a project, these dependencies will be layered on top of
2146+
/// the project environment in a separate, ephemeral environment. These
2147+
/// dependencies are allowed to conflict with those specified by the project.
2148+
#[arg(long)]
2149+
pub with_editable: Vec<String>,
2150+
21432151
/// Run with all packages listed in the given `requirements.txt` files.
21442152
///
21452153
/// The same environment semantics as `--with` apply.

crates/uv/src/commands/project/run.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,16 @@ pub(crate) async fn run(
554554
eprint!("{report:?}");
555555
return Ok(ExitStatus::Failure);
556556
}
557-
Err(err) => return Err(err.into()),
557+
Err(ProjectError::Operation(operations::Error::Named(err))) => {
558+
let err = miette::Report::msg(format!("{err}"))
559+
.context("Invalid `--with` requirement");
560+
eprint!("{err:?}");
561+
return Ok(ExitStatus::Failure);
562+
}
563+
564+
Err(err) => {
565+
return Err(err.into());
566+
}
558567
};
559568

560569
environment.into()

crates/uv/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,11 @@ async fn run_project(
10111011
.with
10121012
.into_iter()
10131013
.map(RequirementsSource::from_package)
1014+
.chain(
1015+
args.with_editable
1016+
.into_iter()
1017+
.map(RequirementsSource::Editable),
1018+
)
10141019
.chain(
10151020
args.with_requirements
10161021
.into_iter()

crates/uv/src/settings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ pub(crate) struct RunSettings {
193193
pub(crate) dev: bool,
194194
pub(crate) command: ExternalCommand,
195195
pub(crate) with: Vec<String>,
196+
pub(crate) with_editable: Vec<String>,
196197
pub(crate) with_requirements: Vec<PathBuf>,
197198
pub(crate) isolated: bool,
198199
pub(crate) show_resolution: bool,
@@ -215,6 +216,7 @@ impl RunSettings {
215216
no_dev,
216217
command,
217218
with,
219+
with_editable,
218220
with_requirements,
219221
isolated,
220222
locked,
@@ -238,6 +240,7 @@ impl RunSettings {
238240
dev: flag(dev, no_dev).unwrap_or(true),
239241
command,
240242
with,
243+
with_editable,
241244
with_requirements: with_requirements
242245
.into_iter()
243246
.filter_map(Maybe::into_option)

crates/uv/tests/run.rs

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use indoc::indoc;
77

88
use uv_python::PYTHON_VERSION_FILENAME;
99

10-
use common::{uv_snapshot, TestContext};
10+
use common::{copy_dir_all, uv_snapshot, TestContext};
1111

1212
mod common;
1313

@@ -538,6 +538,141 @@ fn run_with() -> Result<()> {
538538
Ok(())
539539
}
540540

541+
#[test]
542+
fn run_with_editable() -> Result<()> {
543+
let context = TestContext::new("3.12");
544+
545+
let anyio_local = context.temp_dir.child("src").child("anyio_local");
546+
copy_dir_all(
547+
context.workspace_root.join("scripts/packages/anyio_local"),
548+
&anyio_local,
549+
)?;
550+
551+
let black_editable = context.temp_dir.child("src").child("black_editable");
552+
copy_dir_all(
553+
context
554+
.workspace_root
555+
.join("scripts/packages/black_editable"),
556+
&black_editable,
557+
)?;
558+
559+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
560+
pyproject_toml.write_str(indoc! { r#"
561+
[project]
562+
name = "foo"
563+
version = "1.0.0"
564+
requires-python = ">=3.8"
565+
dependencies = ["anyio", "sniffio==1.3.1"]
566+
"#
567+
})?;
568+
569+
let test_script = context.temp_dir.child("main.py");
570+
test_script.write_str(indoc! { r"
571+
import sniffio
572+
"
573+
})?;
574+
575+
// Requesting an editable requirement should install it in a layer.
576+
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/black_editable").arg("main.py"), @r###"
577+
success: true
578+
exit_code: 0
579+
----- stdout -----
580+
581+
----- stderr -----
582+
Resolved 6 packages in [TIME]
583+
Prepared 4 packages in [TIME]
584+
Installed 4 packages in [TIME]
585+
+ anyio==4.3.0
586+
+ foo==1.0.0 (from file://[TEMP_DIR]/)
587+
+ idna==3.6
588+
+ sniffio==1.3.1
589+
Resolved 1 package in [TIME]
590+
Prepared 1 package in [TIME]
591+
Installed 1 package in [TIME]
592+
+ black==0.1.0 (from file://[TEMP_DIR]/src/black_editable)
593+
"###);
594+
595+
// Requesting an editable requirement should install it in a layer, even if it satisfied
596+
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
597+
success: true
598+
exit_code: 0
599+
----- stdout -----
600+
601+
----- stderr -----
602+
Resolved 6 packages in [TIME]
603+
Audited 4 packages in [TIME]
604+
Resolved 1 package in [TIME]
605+
Prepared 1 package in [TIME]
606+
Installed 1 package in [TIME]
607+
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
608+
"###);
609+
610+
// Requesting the project itself should use the base environment.
611+
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg(".").arg("main.py"), @r###"
612+
success: true
613+
exit_code: 0
614+
----- stdout -----
615+
616+
----- stderr -----
617+
Resolved 6 packages in [TIME]
618+
Audited 4 packages in [TIME]
619+
"###);
620+
621+
// Similarly, an already editable requirement does not require a layer
622+
pyproject_toml.write_str(indoc! { r#"
623+
[project]
624+
name = "foo"
625+
version = "1.0.0"
626+
requires-python = ">=3.8"
627+
dependencies = ["anyio", "sniffio==1.3.1"]
628+
629+
[tool.uv.sources]
630+
anyio = { path = "./src/anyio_local", editable = true }
631+
"#
632+
})?;
633+
634+
uv_snapshot!(context.filters(), context.sync(), @r###"
635+
success: true
636+
exit_code: 0
637+
----- stdout -----
638+
639+
----- stderr -----
640+
Resolved 3 packages in [TIME]
641+
Prepared 1 package in [TIME]
642+
Uninstalled 3 packages in [TIME]
643+
Installed 2 packages in [TIME]
644+
- anyio==4.3.0
645+
+ anyio==4.3.0+foo (from file://[TEMP_DIR]/src/anyio_local)
646+
~ foo==1.0.0 (from file://[TEMP_DIR]/)
647+
- idna==3.6
648+
"###);
649+
650+
uv_snapshot!(context.filters(), context.run().arg("--with-editable").arg("./src/anyio_local").arg("main.py"), @r###"
651+
success: true
652+
exit_code: 0
653+
----- stdout -----
654+
655+
----- stderr -----
656+
Resolved 3 packages in [TIME]
657+
Audited 3 packages in [TIME]
658+
"###);
659+
660+
// If invalid, we should reference `--with-editable`.
661+
uv_snapshot!(context.filters(), context.run().arg("--with").arg("./foo").arg("main.py"), @r###"
662+
success: false
663+
exit_code: 1
664+
----- stdout -----
665+
666+
----- stderr -----
667+
Resolved 3 packages in [TIME]
668+
Audited 3 packages in [TIME]
669+
× Invalid `--with` requirement
670+
╰─▶ Distribution not found at: file://[TEMP_DIR]/foo
671+
"###);
672+
673+
Ok(())
674+
}
675+
541676
#[test]
542677
fn run_locked() -> Result<()> {
543678
let context = TestContext::new("3.12");

docs/reference/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ uv run [OPTIONS] <COMMAND>
322322

323323
<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>
324324

325+
</dd><dt><code>--with-editable</code> <i>with-editable</i></dt><dd><p>Run with the given packages installed as editables</p>
326+
327+
<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>
328+
325329
</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>
326330

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

0 commit comments

Comments
 (0)