8000
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/uv-preview/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ pub enum PreviewFeature {
RelocatableEnvsDefault = 1 << 24,
PublishRequireNormalized = 1 << 25,
Audit = 1 << 26,
ProjectDirectoryMustExist = 1 << 27,
}

impl PreviewFeature {
Expand Down Expand Up @@ -226,6 +227,7 @@ impl PreviewFeature {
Self::RelocatableEnvsDefault => "relocatable-envs-default",
Self::PublishRequireNormalized => "publish-require-normalized",
Self::Audit => "audit",
Self::ProjectDirectoryMustExist => "project-directory-must-exist",
}
}
}
Expand Down Expand Up @@ -272,6 +274,7 @@ impl FromStr for PreviewFeature {
"relocatable-envs-default" => Self::RelocatableEnvsDefault,
"publish-require-normalized" => Self::PublishRequireNormalized,
"audit" => Self::Audit,
"project-directory-must-exist" => Self::ProjectDirectoryMustExist,
_ => return Err(PreviewFeatureParseError),
})
}
Expand Down Expand Up @@ -517,6 +520,10 @@ mod tests {
PreviewFeature::PublishRequireNormalized.as_str(),
"publish-require-normalized"
);
assert_eq!(
PreviewFeature::ProjectDirectoryMustExist.as_str(),
"project-directory-must-exist"
);
}

#[test]
Expand Down
51 changes: 50 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use uv_fs::{CWD, Simplified};
#[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges;
use uv_pep508::VersionOrUrl;
use uv_preview::PreviewFeature;
use uv_preview::{Preview, PreviewFeature};
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
use uv_python::PythonRequest;
use uv_requirements::{GroupsSpecification, RequirementsSource};
Expand Down Expand Up @@ -102,6 +102,55 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Load environment variables not handled by Clap
let environment = EnvironmentOptions::new()?;

// Validate that the project directory exists if explicitly provided via --project, except for
// `uv init`, which creates the project directory (separate deprecation).
let skip_project_validation = matches!(
&*cli.command,
Commands::Project(command) if matches!(**command, ProjectCommand::Init(_))
);

if !skip_project_validation {
if let Some(project_path) = cli.top_level.global_args.project.as_ref() {
// Resolve the preview flags until this becomes stabilized. We check CLI args and
// the `UV_PREVIEW` env var, but not workspace config (which requires reading from
// the project directory that may not exist).
let preview = Preview::from_args(
cli.top_level.global_args.preview || environment.preview.value == Some(true),
cli.top_level.global_args.no_preview,
&cli.top_level.global_args.preview_features,
);
if !project_dir.exists() {
if preview.is_enabled(PreviewFeature::ProjectDirectoryMustExist) {
bail!(
"Project directory `{}` does not exist",
project_path.user_display()
);
}
warn_user_once!(
"Project directory `{}` does not exist. \
This will become an error in a future release. \
Use `--preview-features project-directory-must-exist` to error on this now.",
project_path.user_display()
);
} else if !project_dir.is_dir() {
// On Unix, this always fails downstream (e.g., "Not a directory" when
// trying to read `uv.toml`), so we bail with a clear error message.
// On Windows, this is currently non-fatal, so we only error with the
// preview flag.
if cfg!(unix) || preview.is_enabled(PreviewFeature::ProjectDirectoryMustExist) {
bail!(
"Project path `{}` is not a directory",
project_path.user_display()
);
}
warn_user_once!(
"Project path `{}` is not a directory. Use `--preview-features project-directory-must-exist` to error on this.",
project_path.user_display()
);
}
}
}

// The `--isolated` argument is deprecated on preview APIs, and warns on non-preview APIs.
let deprecated_isolated = if cli.top_level.global_args.isolated {
match &*cli.command {
Expand Down
8000
91 changes: 91 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6570,3 +6570,94 @@ fn run_target_workspace_discovery_bare_script() -> Result<()> {

Ok(())
}

/// Using `--project` with a non-existent directory should warn.
#[test]
fn run_project_not_found() {
let context = uv_test::test_context!("3.12");

uv_snapshot!(context.filters(), context.run().arg("--project").arg("/tmp/does-not-exist-uv-test").arg("python").arg("-c").arg("print('hello')"), @"
success: true
exit_code: 0
----- stdout -----
hello

----- stderr -----
warning: Project directory `/tmp/does-not-exist-uv-test` does not exist. This will become an error in a future release. Use `--preview-features project-directory-must-exist` to error on this now.
");
}

/// Using `--project` with a non-existent directory should error with the preview flag.
#[test]
fn run_project_not_found_preview() {
let context = uv_test::test_context!("3.12");

uv_snapshot!(context.filters(), context.run().arg("--preview-features").arg("project-directory-must-exist").arg("--project").arg("/tmp/does-not-exist-uv-test").arg("python").arg("-c").arg("print('hello')"), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Project directory `/tmp/does-not-exist-uv-test` does not exist
");
}

/// Using `--project` with a non-existent directory should error with `UV_PREVIEW=1`.
#[test]
fn run_project_not_found_uv_preview_env() {
let context = uv_test::test_context!("3.12");

uv_snapshot!(context.filters(), context.run().env("UV_PREVIEW", "1").arg("--project").arg("/tmp/does-not-exist-uv-test").arg("python").arg("-c").arg("print('hello')"), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Project directory `/tmp/does-not-exist-uv-test` does not exist
");
}

/// Using `--project` with a file path should error on Unix (it fails downstream anyway).
#[test]
#[cfg(unix)]
fn run_project_is_file() -> Result<()> {
let context = uv_test::test_context!("3.12");

// Create a file instead of a directory.
let file_path = context.temp_dir.child("not-a-directory");
file_path.write_str("")?;

uv_snapshot!(context.filters(), context.run().arg("--project").arg(file_path.path()).arg("python").arg("-c").arg("print('hello')"), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Project path `not-a-directory` is not a directory
");

Ok(())
}

/// Using `--project` with a file path should warn on Windows (where it's currently non-fatal).
#[test]
#[cfg(windows)]
fn run_project_is_file() -> Result<()> {
let context = uv_test::test_context!("3.12");

// Create a file instead of a directory.
let file_path = context.temp_dir.child("not-a-directory");
file_path.write_str("")?;

uv_snapshot!(context.filters(), context.run().arg("--project").arg(file_path.path()).arg("python").arg("-c").arg("print('hello')"), @"
success: true
exit_code: 0
----- stdout -----
hello

----- stderr -----
warning: Project path `not-a-directory` is not a directory. Use `--preview-features project-directory-must-exist` to error on this.
");

Ok(())
}
2 changes: 2 additions & 0 deletions crates/uv/tests/it/show_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8103,6 +8103,7 @@ fn preview_features() {
RelocatableEnvsDefault,
PublishRequireNormalized,
Audit,
ProjectDirectoryMustExist,
],
},
python_preference: Managed,
Expand Down Expand Up @@ -8373,6 +8374,7 @@ fn preview_features() {
RelocatableEnvsDefault,
PublishRequireNormalized,
Audit,
ProjectDirectoryMustExist,
],
},
python_preference: Managed,
Expand Down
Loading
0