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
23 changes: 17 additions & 6 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
}

// 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
Expand All @@ -96,6 +99,14 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.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));

Expand Down Expand Up @@ -133,18 +144,18 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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()
);
}
Expand Down
157 changes: 138 additions & 19 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://github.com/astral-sh/uv/issues/18508>
#[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");
file_path.write_str("")?;
let project_dir = context.temp_dir.child("project");
project_dir.create_dir_all()?;

uv_snapshot!(context.filters(), context.run().arg("--project").arg(file_path.path()).arg("python").arg("-c").arg("print('hello')"), @"
success: false
exit_code: 2
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 -----
error: Project path `not-a-directory` is not a directory
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 path should warn on Windows (where it's currently non-fatal).
/// Using `--project` with a non-`pyproject.toml` file should warn.
#[test]
#[cfg(windows)]
fn run_project_is_file() -> Result<()> {
fn run_project_non_pyproject_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("")?;
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(file_path.path()).arg("python").arg("-c").arg("print('hello')"), @"
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 -----
hello
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("--")
.arg("python")
.arg("--version"), @"
success: false
exit_code: 2
----- stdout -----

----- 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(())
Expand Down
Loading