Skip to content

Commit

Permalink
Add --isolated support to uv run (#5471)
Browse files Browse the repository at this point in the history
## Summary

The culmination of #4730. We now have `uv run --isolated` which always
uses a fresh environment (but includes the workspace dependencies as
needed). This enables you to test with strict isolation (e.g., `uv run
--isolated -p foo` will ensure that `foo` is unable to import anything
that isn't an actual dependency).

Closes #5430.
  • Loading branch information
charliermarsh committed Jul 30, 2024
1 parent ff3bcbb commit 67b3bfa
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 18 deletions.
5 changes: 5 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,11 @@ pub struct RunArgs {
#[arg(long, value_parser = parse_maybe_file_path)]
pub with_requirements: Vec<Maybe<PathBuf>>,

/// Run the tool in an isolated virtual environment, rather than leveraging the base environment
/// for the current project, to enforce strict isolation between dependencies.
#[arg(long)]
pub isolated: bool,

/// Assert that the `uv.lock` will remain unchanged.
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,
Expand Down
63 changes: 51 additions & 12 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ pub(crate) async fn run(
show_resolution: bool,
locked: bool,
frozen: bool,
isolated: bool,
package: Option<PackageName>,
no_project: bool,
extras: ExtrasSpecification,
dev: bool,
python: Option<String>,
settings: ResolverInstallerSettings,
no_project: bool,
preview: PreviewMode,
python_preference: PythonPreference,
python_fetch: PythonFetch,
Expand Down Expand Up @@ -157,6 +158,8 @@ pub(crate) async fn run(
None
};

let temp_dir;

// Discover and sync the base environment.
let base_interpreter = if let Some(script_interpreter) = script_interpreter {
Some(script_interpreter)
Expand Down Expand Up @@ -195,17 +198,53 @@ pub(crate) async fn run(
);
}

let venv = project::get_or_init_environment(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_fetch,
connectivity,
native_tls,
cache,
printer.filter(show_resolution),
)
.await?;
let venv = if isolated {
// If we're isolating the environment, use an ephemeral virtual environment as the
// base environment for the project.
let interpreter = {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);

// Note we force preview on during `uv run` for now since the entire interface is in preview
PythonInstallation::find_or_fetch(
python.as_deref().map(PythonRequest::parse),
EnvironmentPreference::Any,
python_preference,
python_fetch,
&client_builder,
cache,
Some(&reporter),
)
.await?
.into_interpreter()
};

// Create a virtual environment
temp_dir = cache.environment()?;
uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
false,
)?
} else {
// If we're not isolating the environment, reuse the base environment for the
// project.
project::get_or_init_environment(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_fetch,
connectivity,
native_tls,
cache,
printer.filter(show_resolution),
)
.await?
};

let lock = match project::lock::do_safe_lock(
locked,
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,12 +914,13 @@ async fn run_project(
args.show_resolution || globals.verbose > 0,
args.locked,
args.frozen,
args.isolated,
args.package,
args.no_project || globals.isolated,
args.extras,
args.dev,
args.python,
args.settings,
args.no_project || globals.isolated,
globals.preview,
globals.python_preference,
globals.python_fetch,
Expand Down
5 changes: 4 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ pub(crate) struct RunSettings {
pub(crate) command: ExternalCommand,
pub(crate) with: Vec<String>,
pub(crate) with_requirements: Vec<PathBuf>,
pub(crate) isolated: bool,
pub(crate) show_resolution: bool,
pub(crate) package: Option<PackageName>,
pub(crate) no_project: bool,
Expand All @@ -215,7 +216,7 @@ impl RunSettings {
command,
with,
with_requirements,
show_resolution,
isolated,
locked,
frozen,
installer,
Expand All @@ -224,6 +225,7 @@ impl RunSettings {
package,
no_project,
python,
show_resolution,
} = args;

Self {
Expand All @@ -240,6 +242,7 @@ impl RunSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
isolated,
show_resolution,
package,
no_project,
Expand Down
99 changes: 95 additions & 4 deletions crates/uv/tests/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,10 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> {
uv_snapshot!(context.filters(), universal_windows_filters=true, context
.run()
.arg("--preview")
.arg("--package")
.arg("albatross")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r###"
.arg("--package")
.arg("albatross")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r###"
success: true
exit_code: 0
----- stdout -----
Expand All @@ -519,6 +519,97 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> {
Ok(())
}

/// Check that `uv run --isolated` creates isolated virtual environments.
#[test]
fn test_uv_run_isolate() -> Result<()> {
let context = TestContext::new("3.12");
let work_dir = context.temp_dir.join("albatross-root-workspace");

copy_dir_ignore(workspaces_dir().join("albatross-root-workspace"), &work_dir)?;

let mut filters = context.filters();
filters.push((
r"Using Python 3.12.\[X\] interpreter at: .*",
"Using Python 3.12.[X] interpreter at: [PYTHON]",
));

// Install the root package.
uv_snapshot!(context.filters(), universal_windows_filters=true, context
.run()
.arg("--preview")
.arg("--package")
.arg("albatross")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r###"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Resolved 8 packages in [TIME]
Prepared 7 packages in [TIME]
Installed 7 packages in [TIME]
+ albatross==0.1.0 (from file://[TEMP_DIR]/albatross-root-workspace)
+ anyio==4.3.0
+ bird-feeder==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/bird-feeder)
+ idna==3.6
+ seeds==1.0.0 (from file://[TEMP_DIR]/albatross-root-workspace/packages/seeds)
+ sniffio==1.3.1
+ tqdm==4.66.2
"###
);

// Run in `bird-feeder`. We shouldn't be able to import `albatross`, but we _can_ due to our
// virtual environment semantics. Specifically, we only make the changes necessary to run a
// given command, so we don't remove `albatross` from the environment.
uv_snapshot!(filters, context
.run()
.arg("--preview")
.arg("--package")
.arg("bird-feeder")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r###"
success: true
exit_code: 0
----- stdout -----
Success
----- stderr -----
Resolved 8 packages in [TIME]
Audited 5 packages in [TIME]
"###
);

// If we `--isolated`, though, we use an isolated virtual environment, so `albatross` is not
// available.
// TODO(charlie): This should show the resolution output, but `--isolated` is coupled to
// `--no-project` right now.
uv_snapshot!(filters, context
.run()
.arg("--preview")
.arg("--isolated")
.arg("--package")
.arg("bird-feeder")
.arg("check_installed_albatross.py")
.current_dir(&work_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "[TEMP_DIR]/albatross-root-workspace/check_installed_albatross.py", line 1, in <module>
from albatross import fly
ModuleNotFoundError: No module named 'albatross'
"###
);

Ok(())
}

/// Check that the resolution is the same no matter where in the workspace we are.
fn workspace_lock_idempotence(workspace: &str, subdirectories: &[&str]) -> Result<()> {
let mut shared_lock = None;
Expand Down

0 comments on commit 67b3bfa

Please sign in to comment.