Skip to content

Commit 8f015a7

Browse files
committed
Hint at missing project.name
We got user reports where users were confused about why they can't use `[project.urls]` without other `pyproject.toml`. This PR adds a hint that (according to PEP 621), you need to set `project.name` when using any `project` fields`. (PEP 621 also requires `project.version` xor `dynamic = ["version"]`, but we check that later.) The intermediate parsing layer to tell apart syntax errors from schema errors doesn't incur a performance penalty according to epage (toml-rs/toml#778 (comment)). Closes #6419 Closes #6760
1 parent c91c99b commit 8f015a7

File tree

8 files changed

+87
-10
lines changed

8 files changed

+87
-10
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ tokio-stream = { version = "0.1.14" }
146146
tokio-tar = { version = "0.3.1" }
147147
tokio-util = { version = "0.7.10", features = ["compat"] }
148148
toml = { version = "0.8.12" }
149-
toml_edit = { version = "0.22.13" }
149+
toml_edit = { version = "0.22.13", features = ["serde"] }
150150
tracing = { version = "0.1.40" }
151151
tracing-durations-export = { version = "0.3.0", features = ["plot"] }
152152
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry"] }

crates/pypi-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ rkyv = { workspace = true }
2929
serde = { workspace = true }
3030
thiserror = { workspace = true }
3131
toml = { workspace = true }
32+
toml_edit = { workspace = true }
3233
tracing = { workspace = true }
3334
url = { workspace = true }
3435

crates/pypi-types/src/metadata.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::str::FromStr;
66
use indexmap::IndexMap;
77
use itertools::Itertools;
88
use mailparse::{MailHeaderMap, MailParseError};
9+
use serde::de::IntoDeserializer;
910
use serde::{Deserialize, Serialize};
1011
use thiserror::Error;
1112
use tracing::warn;
@@ -44,8 +45,12 @@ pub struct Metadata23 {
4445
pub enum MetadataError {
4546
#[error(transparent)]
4647
MailParse(#[from] MailParseError),
48+
#[error("Invalid `pyproject.toml`")]
49+
InvalidPyprojectTomlSyntax(#[source] toml_edit::TomlError),
50+
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.name` is not set.")]
51+
InvalidPyprojectTomlMissingName(#[source] toml_edit::de::Error),
4752
#[error(transparent)]
48-
Toml(#[from] toml::de::Error),
53+
InvalidPyprojectTomlSchema(toml_edit::de::Error),
4954
#[error("metadata field {0} not found")]
5055
FieldNotFound(&'static str),
5156
#[error("invalid version: {0}")]
@@ -196,7 +201,7 @@ impl Metadata23 {
196201

197202
/// Extract the metadata from a `pyproject.toml` file, as specified in PEP 621.
198203
pub fn parse_pyproject_toml(contents: &str) -> Result<Self, MetadataError> {
199-
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
204+
let pyproject_toml = PyProjectToml::from_toml(contents)?;
200205

201206
let project = pyproject_toml
202207
.project
@@ -279,6 +284,23 @@ struct PyProjectToml {
279284
tool: Option<Tool>,
280285
}
281286

287+
impl PyProjectToml {
288+
fn from_toml(toml: &str) -> Result<Self, MetadataError> {
289+
let pyproject_toml: toml_edit::ImDocument<_> = toml_edit::ImDocument::from_str(toml)
290+
.map_err(MetadataError::InvalidPyprojectTomlSyntax)?;
291+
let pyproject_toml: Self = PyProjectToml::deserialize(pyproject_toml.into_deserializer())
292+
.map_err(|err| {
293+
// TODO(konsti): A typed error would be nicer, this can break on toml upgrades.
294+
if err.message().contains("missing field `name`") {
295+
MetadataError::InvalidPyprojectTomlMissingName(err)
296+
} else {
297+
MetadataError::InvalidPyprojectTomlSchema(err)
298+
}
299+
})?;
300+
Ok(pyproject_toml)
301+
}
302+
}
303+
282304
/// PEP 621 project metadata.
283305
///
284306
/// This is a subset of the full metadata specification, and only includes the fields that are
@@ -435,7 +457,7 @@ pub struct RequiresDist {
435457
impl RequiresDist {
436458
/// Extract the [`RequiresDist`] from a `pyproject.toml` file, as specified in PEP 621.
437459
pub fn parse_pyproject_toml(contents: &str) -> Result<Self, MetadataError> {
438-
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
460+
let pyproject_toml = PyProjectToml::from_toml(contents)?;
439461

440462
let project = pyproject_toml
441463
.project

crates/uv-build/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ serde_json = { workspace = true }
3434
tempfile = { workspace = true }
3535
thiserror = { workspace = true }
3636
tokio = { workspace = true }
37-
toml = { workspace = true }
37+
toml_edit = { workspace = true }
3838
tracing = { workspace = true }
3939
rustc-hash = { workspace = true }
4040

crates/uv-build/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use indoc::formatdoc;
77
use itertools::Itertools;
88
use regex::Regex;
99
use rustc_hash::FxHashMap;
10-
use serde::de::{value, SeqAccess, Visitor};
10+
use serde::de::{value, IntoDeserializer, SeqAccess, Visitor};
1111
use serde::{de, Deserialize, Deserializer};
1212
use std::ffi::OsString;
1313
use std::fmt::{Display, Formatter};
@@ -82,7 +82,9 @@ pub enum Error {
8282
#[error("Invalid source distribution: {0}")]
8383
InvalidSourceDist(String),
8484
#[error("Invalid `pyproject.toml`")]
85-
InvalidPyprojectToml(#[from] toml::de::Error),
85+
InvalidPyprojectTomlSyntax(#[from] toml_edit::TomlError),
86+
#[error("`pyproject.toml` does not match required schema. Note: When using any `[project]` field, at least `project.name` needs to be set.")]
87+
InvalidPyprojectTomlSchema(#[from] toml_edit::de::Error),
8688
#[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")]
8789
EditableSetupPy,
8890
#[error("Failed to install requirements from {0}")]
@@ -563,8 +565,12 @@ impl SourceBuild {
563565
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
564566
match fs::read_to_string(source_tree.join("pyproject.toml")) {
565567
Ok(toml) => {
568+
let pyproject_toml: toml_edit::ImDocument<_> =
569+
toml_edit::ImDocument::from_str(&toml)
570+
.map_err(Error::InvalidPyprojectTomlSyntax)?;
566571
let pyproject_toml: PyProjectToml =
567-
toml::from_str(&toml).map_err(Error::InvalidPyprojectToml)?;
572+
PyProjectToml::deserialize(pyproject_toml.into_deserializer())
573+
.map_err(Error::InvalidPyprojectTomlSchema)?;
568574
let backend = if let Some(build_system) = pyproject_toml.build_system {
569575
Pep517Backend {
570576
// If `build-backend` is missing, inject the legacy setuptools backend, but

crates/uv-tool/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ pathdiff = { workspace = true }
3131
serde = { workspace = true }
3232
thiserror = { workspace = true }
3333
toml = { workspace = true }
34-
toml_edit = { workspace = true, features = ["serde"] }
34+
toml_edit = { workspace = true }
3535
tracing = { workspace = true }

crates/uv/tests/lock.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11996,3 +11996,50 @@ fn lock_conflicting_environment() -> Result<()> {
1199611996

1199711997
Ok(())
1199811998
}
11999+
12000+
#[test]
12001+
fn invalid_project_table_dep() -> Result<()> {
12002+
let context = TestContext::new("3.12");
12003+
12004+
let pyproject_toml = context.temp_dir.child("a/pyproject.toml");
12005+
pyproject_toml.write_str(
12006+
r#"
12007+
[project]
12008+
name = "a"
12009+
version = "0.1.0"
12010+
requires-python = ">=3.12"
12011+
dependencies = ["b"]
12012+
12013+
[tool.uv.sources]
12014+
b = { path = "../b" }
12015+
"#,
12016+
)?;
12017+
12018+
let pyproject_toml = context.temp_dir.child("b/pyproject.toml");
12019+
pyproject_toml.write_str(
12020+
r"
12021+
[project.urls]
12022+
repository = 'https://github.com/octocat/octocat-python'
12023+
",
12024+
)?;
12025+
12026+
uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("a")), @r###"
12027+
success: false
12028+
exit_code: 2
12029+
----- stdout -----
12030+
12031+
----- stderr -----
12032+
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
12033+
error: Failed to build: `b @ file://[TEMP_DIR]/b`
12034+
Caused by: Failed to extract static metadata from `pyproject.toml`
12035+
Caused by: `pyproject.toml` is using the `[project]` table, but the required `project.name` is not set.
12036+
Caused by: TOML parse error at line 2, column 10
12037+
|
12038+
2 | [project.urls]
12039+
| ^^^^^^^
12040+
missing field `name`
12041+
12042+
"###);
12043+
12044+
Ok(())
12045+
}

0 commit comments

Comments
 (0)