Allow --project to refer to a pyproject.toml directly and reduce to a warning on other files (#18513)

Closes https://github.com/astral-sh/uv/issues/18508

This unintentionally regressed in
https://github.com/astral-sh/uv/pull/17714 as it appeared that
`--project <file>` always failed but it actually succeeds if the file is
has an ancestor directory with a `pyproject.toml`.

This also removes the warning for `--project [path/]pyproject.toml` as I
think that's a fine use-case.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Zanie Blue
2026-03-16 15:14:31 -05:00
committed by GitHub
parent ebdd2bf4b9
commit add312b679
2 changed files with 166 additions and 36 deletions
+17 -6
View File
@@ -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
@@ -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));
@@ -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()
);
}
+149 -30
View File
@@ -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");
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(())