Skip to content

Commit eaa1882

Browse files
authored
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.
1 parent 8390b31 commit eaa1882

5 files changed

Lines changed: 89 additions & 57 deletions

File tree

crates/uv-build-backend/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ use crate::settings::ModuleName;
3232
pub enum Error {
3333
#[error(transparent)]
3434
Io(#[from] io::Error),
35-
#[error("Invalid pyproject.toml")]
36-
Toml(#[from] toml::de::Error),
37-
#[error("Invalid pyproject.toml")]
35+
#[error("Invalid metadata format in: {}", _0.user_display())]
36+
Toml(PathBuf, #[source] toml::de::Error),
37+
#[error("Invalid project metadata")]
3838
Validation(#[from] ValidationError),
3939
#[error("Invalid module name: {0}")]
4040
InvalidModuleName(String, #[source] IdentifierParseError),

crates/uv-build-backend/src/metadata.rs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,11 @@ impl PyProjectToml {
154154
&self.project.version
155155
}
156156

157-
pub(crate) fn parse(contents: &str) -> Result<Self, Error> {
158-
Ok(toml::from_str(contents)?)
157+
pub(crate) fn parse(path: &Path) -> Result<Self, Error> {
158+
let contents = fs_err::read_to_string(path)?;
159+
let pyproject_toml =
160+
toml::from_str(&contents).map_err(|err| Error::Toml(path.to_path_buf(), err))?;
161+
Ok(pyproject_toml)
159162
}
160163

161164
pub(crate) fn readme(&self) -> Option<&Readme> {
@@ -949,7 +952,7 @@ mod tests {
949952
requires = ["uv_build>=0.4.15,<0.5.0"]
950953
build-backend = "uv_build"
951954
"#;
952-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
955+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
953956
let temp_dir = TempDir::new().unwrap();
954957

955958
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
@@ -1034,7 +1037,7 @@ mod tests {
10341037
"#
10351038
};
10361039

1037-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1040+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
10381041
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
10391042

10401043
assert_snapshot!(metadata.core_metadata_format(), @r###"
@@ -1128,7 +1131,7 @@ mod tests {
11281131
"#
11291132
};
11301133

1131-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1134+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
11321135
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
11331136

11341137
assert_snapshot!(metadata.core_metadata_format(), @r"
@@ -1220,7 +1223,7 @@ mod tests {
12201223
"#
12211224
};
12221225

1223-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1226+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
12241227
let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap();
12251228

12261229
assert_snapshot!(metadata.core_metadata_format(), @r###"
@@ -1281,7 +1284,7 @@ mod tests {
12811284
#[test]
12821285
fn build_system_valid() {
12831286
let contents = extend_project("");
1284-
let pyproject_toml = PyProjectToml::parse(&contents).unwrap();
1287+
let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap();
12851288
assert_snapshot!(
12861289
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
12871290
@""
@@ -1299,7 +1302,7 @@ mod tests {
12991302
requires = ["uv_build"]
13001303
build-backend = "uv_build"
13011304
"#};
1302-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1305+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
13031306
assert_snapshot!(
13041307
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
13051308
@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 {
13171320
requires = ["uv_build>=0.4.15,<0.5.0", "wheel"]
13181321
build-backend = "uv_build"
13191322
"#};
1320-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1323+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
13211324
assert_snapshot!(
13221325
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
13231326
@"Expected a single uv requirement in `build-system.requires`, found ``"
@@ -1335,7 +1338,7 @@ mod tests {
13351338
requires = ["setuptools"]
13361339
build-backend = "uv_build"
13371340
"#};
1338-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1341+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
13391342
assert_snapshot!(
13401343
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
13411344
@"Expected a single uv requirement in `build-system.requires`, found ``"
@@ -1353,7 +1356,7 @@ mod tests {
13531356
requires = ["uv_build>=0.4.15,<0.5.0"]
13541357
build-backend = "setuptools"
13551358
"#};
1356-
let pyproject_toml = PyProjectToml::parse(contents).unwrap();
1359+
let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap();
13571360
assert_snapshot!(
13581361
pyproject_toml.check_build_system("0.4.15+test").join("\n"),
13591362
@r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"###
@@ -1364,7 +1367,7 @@ mod tests {
13641367
fn minimal() {
13651368
let contents = extend_project("");
13661369

1367-
let metadata = PyProjectToml::parse(&contents)
1370+
let metadata = toml::from_str::<PyProjectToml>(&contents)
13681371
.unwrap()
13691372
.to_metadata(Path::new("/do/not/read"))
13701373
.unwrap();
@@ -1383,15 +1386,14 @@ mod tests {
13831386
"#
13841387
});
13851388

1386-
let err = PyProjectToml::parse(&contents).unwrap_err();
1387-
assert_snapshot!(format_err(err), @r###"
1388-
Invalid pyproject.toml
1389-
Caused by: TOML parse error at line 4, column 10
1389+
let err = toml::from_str::<PyProjectToml>(&contents).unwrap_err();
1390+
assert_snapshot!(format_err(err), @r#"
1391+
TOML parse error at line 4, column 10
13901392
|
13911393
4 | readme = { path = "Readme.md" }
13921394
| ^^^^^^^^^^^^^^^^^^^^^^
13931395
data did not match any variant of untagged enum Readme
1394-
"###);
1396+
"#);
13951397
}
13961398

13971399
#[test]
@@ -1401,7 +1403,7 @@ mod tests {
14011403
"#
14021404
});
14031405

1404-
let err = PyProjectToml::parse(&contents)
1406+
let err = toml::from_str::<PyProjectToml>(&contents)
14051407
.unwrap()
14061408
.to_metadata(Path::new("/do/not/read"))
14071409
.unwrap_err();
@@ -1423,14 +1425,14 @@ mod tests {
14231425
"#
14241426
});
14251427

1426-
let err = PyProjectToml::parse(&contents)
1428+
let err = toml::from_str::<PyProjectToml>(&contents)
14271429
.unwrap()
14281430
.to_metadata(Path::new("/do/not/read"))
14291431
.unwrap_err();
1430-
assert_snapshot!(format_err(err), @r###"
1431-
Invalid pyproject.toml
1432+
assert_snapshot!(format_err(err), @r"
1433+
Invalid project metadata
14321434
Caused by: `project.description` must be a single line
1433-
"###);
1435+
");
14341436
}
14351437

14361438
#[test]
@@ -1441,14 +1443,14 @@ mod tests {
14411443
"#
14421444
});
14431445

1444-
let err = PyProjectToml::parse(&contents)
1446+
let err = toml::from_str::<PyProjectToml>(&contents)
14451447
.unwrap()
14461448
.to_metadata(Path::new("/do/not/read"))
14471449
.unwrap_err();
1448-
assert_snapshot!(format_err(err), @r###"
1449-
Invalid pyproject.toml
1450+
assert_snapshot!(format_err(err), @r"
1451+
Invalid project metadata
14501452
Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string
1451-
"###);
1453+
");
14521454
}
14531455

14541456
#[test]
@@ -1457,7 +1459,7 @@ mod tests {
14571459
license = "MIT OR Apache-2.0"
14581460
"#
14591461
});
1460-
let metadata = PyProjectToml::parse(&contents)
1462+
let metadata = toml::from_str::<PyProjectToml>(&contents)
14611463
.unwrap()
14621464
.to_metadata(Path::new("/do/not/read"))
14631465
.unwrap();
@@ -1475,13 +1477,13 @@ mod tests {
14751477
license = "MIT XOR Apache-2"
14761478
"#
14771479
});
1478-
let err = PyProjectToml::parse(&contents)
1480+
let err = toml::from_str::<PyProjectToml>(&contents)
14791481
.unwrap()
14801482
.to_metadata(Path::new("/do/not/read"))
14811483
.unwrap_err();
14821484
// TODO(konsti): We mess up the indentation in the error.
14831485
assert_snapshot!(format_err(err), @r"
1484-
Invalid pyproject.toml
1486+
Invalid project metadata
14851487
Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2
14861488
Caused by: MIT XOR Apache-2
14871489
^^^ unknown term
@@ -1495,18 +1497,18 @@ mod tests {
14951497
"#
14961498
});
14971499

1498-
let err = PyProjectToml::parse(&contents)
1500+
let err = toml::from_str::<PyProjectToml>(&contents)
14991501
.unwrap()
15001502
.to_metadata(Path::new("/do/not/read"))
15011503
.unwrap_err();
1502-
assert_snapshot!(format_err(err), @r###"
1503-
Invalid pyproject.toml
1504+
assert_snapshot!(format_err(err), @r"
1505+
Invalid project metadata
15041506
Caused by: Dynamic metadata is not supported
1505-
"###);
1507+
");
15061508
}
15071509

15081510
fn script_error(contents: &str) -> String {
1509-
let err = PyProjectToml::parse(contents)
1511+
let err = toml::from_str::<PyProjectToml>(contents)
15101512
.unwrap()
15111513
.to_entry_points()
15121514
.unwrap_err();

crates/uv-build-backend/src/source_dist.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ pub fn build_source_dist(
2626
uv_version: &str,
2727
show_warnings: bool,
2828
) -> Result<SourceDistFilename, Error> {
29-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
30-
let pyproject_toml = PyProjectToml::parse(&contents)?;
29+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
3130
let filename = SourceDistFilename {
3231
name: pyproject_toml.name().clone(),
3332
version: pyproject_toml.version().clone(),
@@ -45,8 +44,7 @@ pub fn list_source_dist(
4544
uv_version: &str,
4645
show_warnings: bool,
4746
) -> Result<(SourceDistFilename, FileList), Error> {
48-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
49-
let pyproject_toml = PyProjectToml::parse(&contents)?;
47+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
5048
let filename = SourceDistFilename {
5149
name: pyproject_toml.name().clone(),
5250
version: pyproject_toml.version().clone(),
@@ -188,8 +186,7 @@ fn write_source_dist(
188186
uv_version: &str,
189187
show_warnings: bool,
190188
) -> Result<SourceDistFilename, Error> {
191-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
192-
let pyproject_toml = PyProjectToml::parse(&contents)?;
189+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
193190
for warning in pyproject_toml.check_build_system(uv_version) {
194191
warn_user_once!("{warning}");
195192
}

crates/uv-build-backend/src/wheel.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ pub fn build_wheel(
3131
uv_version: &str,
3232
show_warnings: bool,
3333
) -> Result<WheelFilename, Error> {
34-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
35-
let pyproject_toml = PyProjectToml::parse(&contents)?;
34+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
3635
for warning in pyproject_toml.check_build_system(uv_version) {
3736
warn_user_once!("{warning}");
3837
}
@@ -71,8 +70,7 @@ pub fn list_wheel(
7170
uv_version: &str,
7271
show_warnings: bool,
7372
) -> Result<(WheelFilename, FileList), Error> {
74-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
75-
let pyproject_toml = PyProjectToml::parse(&contents)?;
73+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
7674
for warning in pyproject_toml.check_build_system(uv_version) {
7775
warn_user_once!("{warning}");
7876
}
@@ -273,8 +271,7 @@ pub fn build_editable(
273271
uv_version: &str,
274272
show_warnings: bool,
275273
) -> Result<WheelFilename, Error> {
276-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
277-
let pyproject_toml = PyProjectToml::parse(&contents)?;
274+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
278275
for warning in pyproject_toml.check_build_system(uv_version) {
279276
warn_user_once!("{warning}");
280277
}
@@ -335,8 +332,7 @@ pub fn metadata(
335332
metadata_directory: &Path,
336333
uv_version: &str,
337334
) -> Result<String, Error> {
338-
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
339-
let pyproject_toml = PyProjectToml::parse(&contents)?;
335+
let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?;
340336
for warning in pyproject_toml.check_build_system(uv_version) {
341337
warn_user_once!("{warning}");
342338
}

crates/uv/tests/it/build_backend.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -792,15 +792,15 @@ fn license_glob_without_matches_errors() -> Result<()> {
792792
.build_backend()
793793
.arg("build-wheel")
794794
.arg(context.temp_dir.path())
795-
.current_dir(project.path()), @r###"
795+
.current_dir(project.path()), @r"
796796
success: false
797797
exit_code: 2
798798
----- stdout -----
799799
800800
----- stderr -----
801-
error: Invalid pyproject.toml
801+
error: Invalid project metadata
802802
Caused by: `project.license-files` glob `abc` did not match any files
803-
"###);
803+
");
804804

805805
Ok(())
806806
}
@@ -835,15 +835,15 @@ fn license_file_must_be_utf8() -> Result<()> {
835835
.build_backend()
836836
.arg("build-wheel")
837837
.arg(context.temp_dir.path())
838-
.current_dir(project.path()), @r###"
838+
.current_dir(project.path()), @r"
839839
success: false
840840
exit_code: 2
841841
----- stdout -----
842842
843843
----- stderr -----
844-
error: Invalid pyproject.toml
844+
error: Invalid project metadata
845845
Caused by: License file `LICENSE.bin` must be UTF-8 encoded
846-
"###);
846+
");
847847

848848
Ok(())
849849
}
@@ -1185,3 +1185,40 @@ fn warn_on_redundant_module_names() -> Result<()> {
11851185

11861186
Ok(())
11871187
}
1188+
1189+
#[test]
1190+
fn invalid_pyproject_toml() -> Result<()> {
1191+
let context = TestContext::new("3.12");
1192+
1193+
context
1194+
.temp_dir
1195+
.child("child")
1196+
.child("pyproject.toml")
1197+
.write_str(indoc! {r#"
1198+
[project]
1199+
name = 1
1200+
version = "1.0.0"
1201+
1202+
[build-system]
1203+
requires = ["uv_build>=0.9,<10000"]
1204+
build-backend = "uv_build"
1205+
"#})?;
1206+
1207+
uv_snapshot!(context.filters(), context.build().arg("child"), @r"
1208+
success: false
1209+
exit_code: 2
1210+
----- stdout -----
1211+
1212+
----- stderr -----
1213+
Building source distribution (uv build backend)...
1214+
× Failed to build `[TEMP_DIR]/child`
1215+
├─▶ Invalid metadata format in: child/pyproject.toml
1216+
╰─▶ TOML parse error at line 2, column 8
1217+
|
1218+
2 | name = 1
1219+
| ^
1220+
invalid type: integer `1`, expected a string
1221+
");
1222+
1223+
Ok(())
1224+
}

0 commit comments

Comments
 (0)