mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-29 03:02:55 +00:00
Add --isolated support to uv run (#5471)
## 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.
This commit is contained in:
parent
ff3bcbb639
commit
67b3bfa213
5 changed files with 157 additions and 18 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -215,7 +216,7 @@ impl RunSettings {
|
|||
command,
|
||||
with,
|
||||
with_requirements,
|
||||
show_resolution,
|
||||
isolated,
|
||||
locked,
|
||||
frozen,
|
||||
installer,
|
||||
|
|
@ -224,6 +225,7 @@ impl RunSettings {
|
|||
package,
|
||||
no_project,
|
||||
python,
|
||||
show_resolution,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
|
|
@ -240,6 +242,7 @@ impl RunSettings {
|
|||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
.collect(),
|
||||
isolated,
|
||||
show_resolution,
|
||||
package,
|
||||
no_project,
|
||||
|
|
|
|||
|
|
@ -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 -----
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue