diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e3885a9c1..455c8eab1 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -88,6 +88,9 @@ async fn run(mut cli: Cli) -> Result { } // Determine the project directory. + // + // If `--project` points to a `pyproject.toml` file, resolve to its parent directory, + // since downstream code (e.g., `FilesystemOptions::find`) expects a directory. let project_dir = cli .top_level .global_args @@ -96,6 +99,14 @@ async fn run(mut cli: Cli) -> Result { .map(std::path::absolute) .transpose()? .map(uv_fs::normalize_path_buf) + .map(|path| { + path.file_name() + .filter(|name| *name == "pyproject.toml") + .filter(|_| path.is_file()) + .and_then(|_| path.parent()) + .map(Path::to_path_buf) + .unwrap_or(path) + }) .map(Cow::Owned) .unwrap_or_else(|| Cow::Borrowed(&*CWD)); @@ -133,18 +144,18 @@ async fn run(mut cli: Cli) -> Result { 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) { + // `--project path/to/pyproject.toml` is resolved to its parent above, + // so this only triggers for other file types (see #18508). + if 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 `{}` is not a directory. \ + 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() ); } diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 44d1ea2f0..938e9eb3d 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -6617,46 +6617,165 @@ fn run_project_not_found_uv_preview_env() { "); } -/// Using `--project` with a file path should error on Unix (it fails downstream anyway). +/// When `--project` points to a `pyproject.toml` file, resolve to its parent directory without +/// warning. +/// +/// See: #[test] -#[cfg(unix)] -fn run_project_is_file() -> Result<()> { +fn run_project_pyproject_toml_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"); + let project_dir = context.temp_dir.child("project"); + project_dir.create_dir_all()?; + + let pyproject_toml = project_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + // Passing `--project project/pyproject.toml` should resolve to the parent directory without warning. + uv_snapshot!(context.filters(), context.run() + .arg("--project") + .arg(project_dir.join("pyproject.toml")) + .env_remove(EnvVars::VIRTUAL_ENV) + .arg("--") + .arg("python") + .arg("--version"), @" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: project/.venv + Resolved 1 package in [TIME] + Checked in [TIME] + "); + + Ok(()) +} + +/// Using `--project` with a non-`pyproject.toml` file should warn. +#[test] +fn run_project_non_pyproject_file() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let project_dir = context.temp_dir.child("project"); + project_dir.create_dir_all()?; + + let pyproject_toml = project_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + let other_file = project_dir.child("README.md"); + other_file.write_str("")?; + + uv_snapshot!(context.filters(), context.run() + .arg("--project") + .arg(other_file.path()) + .env_remove(EnvVars::VIRTUAL_ENV) + .arg("--") + .arg("python") + .arg("--version"), @" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + warning: Project path `project/README.md` is not a directory. This will become an error in a future release. Use `--preview-features project-directory-must-exist` to error on this now. + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: project/.venv + Resolved 1 package in [TIME] + Checked in [TIME] + "); + + Ok(()) +} + +/// Using `--project` with a nested non-`pyproject.toml` file should warn. Workspace discovery +/// walks ancestors to find the `pyproject.toml`. +#[test] +fn run_project_nested_file() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let project_dir = context.temp_dir.child("project"); + project_dir.create_dir_all()?; + + let pyproject_toml = project_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + let subdir = project_dir.child("subdir"); + subdir.create_dir_all()?; + let nested_file = subdir.child("somefile"); + nested_file.write_str("")?; + + uv_snapshot!(context.filters(), context.run() + .arg("--project") + .arg(nested_file.path()) + .env_remove(EnvVars::VIRTUAL_ENV) + .arg("--") + .arg("python") + .arg("--version"), @" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + warning: Project path `project/subdir/somefile` is not a directory. This will become an error in a future release. Use `--preview-features project-directory-must-exist` to error on this now. + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: project/.venv + Resolved 1 package in [TIME] + Checked in [TIME] + "); + + Ok(()) +} + +/// Using `--project` with a file that has no ancestor project should warn, then fail downstream. +#[test] +#[cfg(unix)] +fn run_project_file_no_ancestor_project() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let isolated_dir = context.temp_dir.child("isolated"); + isolated_dir.create_dir_all()?; + let file_path = isolated_dir.child("somefile"); file_path.write_str("")?; - uv_snapshot!(context.filters(), context.run().arg("--project").arg(file_path.path()).arg("python").arg("-c").arg("print('hello')"), @" + uv_snapshot!(context.filters(), context.run() + .arg("--project") + .arg(file_path.path()) + .arg("--") + .arg("python") + .arg("--version"), @" 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. + warning: Project path `isolated/somefile` is not a directory. This will become an error in a future release. Use `--preview-features project-directory-must-exist` to error on this now. + error: failed to open file `[TEMP_DIR]/isolated/somefile/uv.toml`: Not a directory (os error 20) "); Ok(())