From eaa1882c517f8df90b617236c81fca09bcd908c9 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 5 Dec 2025 16:14:20 +0100 Subject: [PATCH] Tweak language for build backend validation errors (#16720) Validation errors can also come from files pulled in by `pyproject.toml`, and `pyproject.toml` can be in a subdirectory. --- crates/uv-build-backend/src/lib.rs | 6 +- crates/uv-build-backend/src/metadata.rs | 70 +++++++++++----------- crates/uv-build-backend/src/source_dist.rs | 9 +-- crates/uv-build-backend/src/wheel.rs | 12 ++-- crates/uv/tests/it/build_backend.rs | 49 +++++++++++++-- 5 files changed, 89 insertions(+), 57 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 852e8c651..01f7de2b1 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -32,9 +32,9 @@ use crate::settings::ModuleName; pub enum Error { #[error(transparent)] Io(#[from] io::Error), - #[error("Invalid pyproject.toml")] - Toml(#[from] toml::de::Error), - #[error("Invalid pyproject.toml")] + #[error("Invalid metadata format in: {}", _0.user_display())] + Toml(PathBuf, #[source] toml::de::Error), + #[error("Invalid project metadata")] Validation(#[from] ValidationError), #[error("Invalid module name: {0}")] InvalidModuleName(String, #[source] IdentifierParseError), diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index 8ccd566fb..e45670a6e 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -154,8 +154,11 @@ impl PyProjectToml { &self.project.version } - pub(crate) fn parse(contents: &str) -> Result { - Ok(toml::from_str(contents)?) + pub(crate) fn parse(path: &Path) -> Result { + let contents = fs_err::read_to_string(path)?; + let pyproject_toml = + toml::from_str(&contents).map_err(|err| Error::Toml(path.to_path_buf(), err))?; + Ok(pyproject_toml) } pub(crate) fn readme(&self) -> Option<&Readme> { @@ -949,7 +952,7 @@ mod tests { requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "uv_build" "#; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let temp_dir = TempDir::new().unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); @@ -1034,7 +1037,7 @@ mod tests { "# }; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" @@ -1128,7 +1131,7 @@ mod tests { "# }; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r" @@ -1220,7 +1223,7 @@ mod tests { "# }; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" @@ -1281,7 +1284,7 @@ mod tests { #[test] fn build_system_valid() { let contents = extend_project(""); - let pyproject_toml = PyProjectToml::parse(&contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"" @@ -1299,7 +1302,7 @@ mod tests { requires = ["uv_build"] build-backend = "uv_build" "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @r###"`build_system.requires = ["uv_build"]` is missing an upper bound on the `uv_build` version such as `<0.5`. Without bounding the `uv_build` version, the source distribution will break when a future, breaking version of `uv_build` is released."### @@ -1317,7 +1320,7 @@ mod tests { requires = ["uv_build>=0.4.15,<0.5.0", "wheel"] build-backend = "uv_build" "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"Expected a single uv requirement in `build-system.requires`, found ``" @@ -1335,7 +1338,7 @@ mod tests { requires = ["setuptools"] build-backend = "uv_build" "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"Expected a single uv requirement in `build-system.requires`, found ``" @@ -1353,7 +1356,7 @@ mod tests { requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "setuptools" "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"### @@ -1364,7 +1367,7 @@ mod tests { fn minimal() { let contents = extend_project(""); - let metadata = PyProjectToml::parse(&contents) + let metadata = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap(); @@ -1383,15 +1386,14 @@ mod tests { "# }); - let err = PyProjectToml::parse(&contents).unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: TOML parse error at line 4, column 10 + let err = toml::from_str::(&contents).unwrap_err(); + assert_snapshot!(format_err(err), @r#" + TOML parse error at line 4, column 10 | 4 | readme = { path = "Readme.md" } | ^^^^^^^^^^^^^^^^^^^^^^ data did not match any variant of untagged enum Readme - "###); + "#); } #[test] @@ -1401,7 +1403,7 @@ mod tests { "# }); - let err = PyProjectToml::parse(&contents) + let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); @@ -1423,14 +1425,14 @@ mod tests { "# }); - let err = PyProjectToml::parse(&contents) + let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml + assert_snapshot!(format_err(err), @r" + Invalid project metadata Caused by: `project.description` must be a single line - "###); + "); } #[test] @@ -1441,14 +1443,14 @@ mod tests { "# }); - let err = PyProjectToml::parse(&contents) + let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml + assert_snapshot!(format_err(err), @r" + Invalid project metadata Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string - "###); + "); } #[test] @@ -1457,7 +1459,7 @@ mod tests { license = "MIT OR Apache-2.0" "# }); - let metadata = PyProjectToml::parse(&contents) + let metadata = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap(); @@ -1475,13 +1477,13 @@ mod tests { license = "MIT XOR Apache-2" "# }); - let err = PyProjectToml::parse(&contents) + let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); // TODO(konsti): We mess up the indentation in the error. assert_snapshot!(format_err(err), @r" - Invalid pyproject.toml + Invalid project metadata Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2 Caused by: MIT XOR Apache-2 ^^^ unknown term @@ -1495,18 +1497,18 @@ mod tests { "# }); - let err = PyProjectToml::parse(&contents) + let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml + assert_snapshot!(format_err(err), @r" + Invalid project metadata Caused by: Dynamic metadata is not supported - "###); + "); } fn script_error(contents: &str) -> String { - let err = PyProjectToml::parse(contents) + let err = toml::from_str::(contents) .unwrap() .to_entry_points() .unwrap_err(); diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 7059985ea..5a3e77b5d 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -26,8 +26,7 @@ pub fn build_source_dist( uv_version: &str, show_warnings: bool, ) -> Result { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let filename = SourceDistFilename { name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), @@ -45,8 +44,7 @@ pub fn list_source_dist( uv_version: &str, show_warnings: bool, ) -> Result<(SourceDistFilename, FileList), Error> { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let filename = SourceDistFilename { name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), @@ -188,8 +186,7 @@ fn write_source_dist( uv_version: &str, show_warnings: bool, ) -> Result { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 031756a3b..dd072fabf 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -31,8 +31,7 @@ pub fn build_wheel( uv_version: &str, show_warnings: bool, ) -> Result { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } @@ -71,8 +70,7 @@ pub fn list_wheel( uv_version: &str, show_warnings: bool, ) -> Result<(WheelFilename, FileList), Error> { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } @@ -273,8 +271,7 @@ pub fn build_editable( uv_version: &str, show_warnings: bool, ) -> Result { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } @@ -335,8 +332,7 @@ pub fn metadata( metadata_directory: &Path, uv_version: &str, ) -> Result { - let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; - let pyproject_toml = PyProjectToml::parse(&contents)?; + let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 608eaadbb..232ea572a 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -792,15 +792,15 @@ fn license_glob_without_matches_errors() -> Result<()> { .build_backend() .arg("build-wheel") .arg(context.temp_dir.path()) - .current_dir(project.path()), @r###" + .current_dir(project.path()), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Invalid pyproject.toml + error: Invalid project metadata Caused by: `project.license-files` glob `abc` did not match any files - "###); + "); Ok(()) } @@ -835,15 +835,15 @@ fn license_file_must_be_utf8() -> Result<()> { .build_backend() .arg("build-wheel") .arg(context.temp_dir.path()) - .current_dir(project.path()), @r###" + .current_dir(project.path()), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Invalid pyproject.toml + error: Invalid project metadata Caused by: License file `LICENSE.bin` must be UTF-8 encoded - "###); + "); Ok(()) } @@ -1185,3 +1185,40 @@ fn warn_on_redundant_module_names() -> Result<()> { Ok(()) } + +#[test] +fn invalid_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.12"); + + context + .temp_dir + .child("child") + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = 1 + version = "1.0.0" + + [build-system] + requires = ["uv_build>=0.9,<10000"] + build-backend = "uv_build" + "#})?; + + uv_snapshot!(context.filters(), context.build().arg("child"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + × Failed to build `[TEMP_DIR]/child` + ├─▶ Invalid metadata format in: child/pyproject.toml + ╰─▶ TOML parse error at line 2, column 8 + | + 2 | name = 1 + | ^ + invalid type: integer `1`, expected a string + "); + + Ok(()) +}