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.
This commit is contained in:
konsti 2025-12-05 16:14:20 +01:00 committed by GitHub
parent 8390b311f8
commit eaa1882c51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 57 deletions

View File

@ -32,9 +32,9 @@ use crate::settings::ModuleName;
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Invalid pyproject.toml")] #[error("Invalid metadata format in: {}", _0.user_display())]
Toml(#[from] toml::de::Error), Toml(PathBuf, #[source] toml::de::Error),
#[error("Invalid pyproject.toml")] #[error("Invalid project metadata")]
Validation(#[from] ValidationError), Validation(#[from] ValidationError),
#[error("Invalid module name: {0}")] #[error("Invalid module name: {0}")]
InvalidModuleName(String, #[source] IdentifierParseError), InvalidModuleName(String, #[source] IdentifierParseError),

View File

@ -154,8 +154,11 @@ impl PyProjectToml {
&self.project.version &self.project.version
} }
pub(crate) fn parse(contents: &str) -> Result<Self, Error> { pub(crate) fn parse(path: &Path) -> Result<Self, Error> {
Ok(toml::from_str(contents)?) 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> { pub(crate) fn readme(&self) -> Option<&Readme> {
@ -949,7 +952,7 @@ mod tests {
requires = ["uv_build>=0.4.15,<0.5.0"] requires = ["uv_build>=0.4.15,<0.5.0"]
build-backend = "uv_build" 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 temp_dir = TempDir::new().unwrap();
let metadata = pyproject_toml.to_metadata(temp_dir.path()).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(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r###" 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(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r" 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(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
assert_snapshot!(metadata.core_metadata_format(), @r###" assert_snapshot!(metadata.core_metadata_format(), @r###"
@ -1281,7 +1284,7 @@ mod tests {
#[test] #[test]
fn build_system_valid() { fn build_system_valid() {
let contents = extend_project(""); let contents = extend_project("");
let pyproject_toml = PyProjectToml::parse(&contents).unwrap(); let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"" @""
@ -1299,7 +1302,7 @@ mod tests {
requires = ["uv_build"] requires = ["uv_build"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml = PyProjectToml::parse(contents).unwrap(); let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), 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."### @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"] requires = ["uv_build>=0.4.15,<0.5.0", "wheel"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml = PyProjectToml::parse(contents).unwrap(); let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"Expected a single uv requirement in `build-system.requires`, found ``" @"Expected a single uv requirement in `build-system.requires`, found ``"
@ -1335,7 +1338,7 @@ mod tests {
requires = ["setuptools"] requires = ["setuptools"]
build-backend = "uv_build" build-backend = "uv_build"
"#}; "#};
let pyproject_toml = PyProjectToml::parse(contents).unwrap(); let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), pyproject_toml.check_build_system("0.4.15+test").join("\n"),
@"Expected a single uv requirement in `build-system.requires`, found ``" @"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"] requires = ["uv_build>=0.4.15,<0.5.0"]
build-backend = "setuptools" build-backend = "setuptools"
"#}; "#};
let pyproject_toml = PyProjectToml::parse(contents).unwrap(); let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
assert_snapshot!( assert_snapshot!(
pyproject_toml.check_build_system("0.4.15+test").join("\n"), 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"`"### @r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"###
@ -1364,7 +1367,7 @@ mod tests {
fn minimal() { fn minimal() {
let contents = extend_project(""); let contents = extend_project("");
let metadata = PyProjectToml::parse(&contents) let metadata = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap(); .unwrap();
@ -1383,15 +1386,14 @@ mod tests {
"# "#
}); });
let err = PyProjectToml::parse(&contents).unwrap_err(); let err = toml::from_str::<PyProjectToml>(&contents).unwrap_err();
assert_snapshot!(format_err(err), @r###" assert_snapshot!(format_err(err), @r#"
Invalid pyproject.toml TOML parse error at line 4, column 10
Caused by: TOML parse error at line 4, column 10
| |
4 | readme = { path = "Readme.md" } 4 | readme = { path = "Readme.md" }
| ^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^
data did not match any variant of untagged enum Readme data did not match any variant of untagged enum Readme
"###); "#);
} }
#[test] #[test]
@ -1401,7 +1403,7 @@ mod tests {
"# "#
}); });
let err = PyProjectToml::parse(&contents) let err = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
@ -1423,14 +1425,14 @@ mod tests {
"# "#
}); });
let err = PyProjectToml::parse(&contents) let err = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r###" assert_snapshot!(format_err(err), @r"
Invalid pyproject.toml Invalid project metadata
Caused by: `project.description` must be a single line Caused by: `project.description` must be a single line
"###); ");
} }
#[test] #[test]
@ -1441,14 +1443,14 @@ mod tests {
"# "#
}); });
let err = PyProjectToml::parse(&contents) let err = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r###" assert_snapshot!(format_err(err), @r"
Invalid pyproject.toml Invalid project metadata
Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string
"###); ");
} }
#[test] #[test]
@ -1457,7 +1459,7 @@ mod tests {
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
"# "#
}); });
let metadata = PyProjectToml::parse(&contents) let metadata = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap(); .unwrap();
@ -1475,13 +1477,13 @@ mod tests {
license = "MIT XOR Apache-2" license = "MIT XOR Apache-2"
"# "#
}); });
let err = PyProjectToml::parse(&contents) let err = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
// TODO(konsti): We mess up the indentation in the error. // TODO(konsti): We mess up the indentation in the error.
assert_snapshot!(format_err(err), @r" 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: `project.license` is not a valid SPDX expression: MIT XOR Apache-2
Caused by: MIT XOR Apache-2 Caused by: MIT XOR Apache-2
^^^ unknown term ^^^ unknown term
@ -1495,18 +1497,18 @@ mod tests {
"# "#
}); });
let err = PyProjectToml::parse(&contents) let err = toml::from_str::<PyProjectToml>(&contents)
.unwrap() .unwrap()
.to_metadata(Path::new("/do/not/read")) .to_metadata(Path::new("/do/not/read"))
.unwrap_err(); .unwrap_err();
assert_snapshot!(format_err(err), @r###" assert_snapshot!(format_err(err), @r"
Invalid pyproject.toml Invalid project metadata
Caused by: Dynamic metadata is not supported Caused by: Dynamic metadata is not supported
"###); ");
} }
fn script_error(contents: &str) -> String { fn script_error(contents: &str) -> String {
let err = PyProjectToml::parse(contents) let err = toml::from_str::<PyProjectToml>(contents)
.unwrap() .unwrap()
.to_entry_points() .to_entry_points()
.unwrap_err(); .unwrap_err();

View File

@ -26,8 +26,7 @@ pub fn build_source_dist(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
let filename = SourceDistFilename { let filename = SourceDistFilename {
name: pyproject_toml.name().clone(), name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(), version: pyproject_toml.version().clone(),
@ -45,8 +44,7 @@ pub fn list_source_dist(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<(SourceDistFilename, FileList), Error> { ) -> Result<(SourceDistFilename, FileList), Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
let filename = SourceDistFilename { let filename = SourceDistFilename {
name: pyproject_toml.name().clone(), name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(), version: pyproject_toml.version().clone(),
@ -188,8 +186,7 @@ fn write_source_dist(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }

View File

@ -31,8 +31,7 @@ pub fn build_wheel(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -71,8 +70,7 @@ pub fn list_wheel(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<(WheelFilename, FileList), Error> { ) -> Result<(WheelFilename, FileList), Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -273,8 +271,7 @@ pub fn build_editable(
uv_version: &str, uv_version: &str,
show_warnings: bool, show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }
@ -335,8 +332,7 @@ pub fn metadata(
metadata_directory: &Path, metadata_directory: &Path,
uv_version: &str, uv_version: &str,
) -> Result<String, Error> { ) -> Result<String, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
for warning in pyproject_toml.check_build_system(uv_version) { for warning in pyproject_toml.check_build_system(uv_version) {
warn_user_once!("{warning}"); warn_user_once!("{warning}");
} }

View File

@ -792,15 +792,15 @@ fn license_glob_without_matches_errors() -> Result<()> {
.build_backend() .build_backend()
.arg("build-wheel") .arg("build-wheel")
.arg(context.temp_dir.path()) .arg(context.temp_dir.path())
.current_dir(project.path()), @r###" .current_dir(project.path()), @r"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Invalid pyproject.toml error: Invalid project metadata
Caused by: `project.license-files` glob `abc` did not match any files Caused by: `project.license-files` glob `abc` did not match any files
"###); ");
Ok(()) Ok(())
} }
@ -835,15 +835,15 @@ fn license_file_must_be_utf8() -> Result<()> {
.build_backend() .build_backend()
.arg("build-wheel") .arg("build-wheel")
.arg(context.temp_dir.path()) .arg(context.temp_dir.path())
.current_dir(project.path()), @r###" .current_dir(project.path()), @r"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Invalid pyproject.toml error: Invalid project metadata
Caused by: License file `LICENSE.bin` must be UTF-8 encoded Caused by: License file `LICENSE.bin` must be UTF-8 encoded
"###); ");
Ok(()) Ok(())
} }
@ -1185,3 +1185,40 @@ fn warn_on_redundant_module_names() -> Result<()> {
Ok(()) 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(())
}