Skip to content

Commit

Permalink
Hint at missing project.name
Browse files Browse the repository at this point in the history
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
  • Loading branch information
konstin committed Aug 29, 2024
1 parent 3be4fe5 commit f7f010d
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 8 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ tokio-stream = { version = "0.1.14" }
tokio-tar = { version = "0.3.1" }
tokio-util = { version = "0.7.10", features = ["compat"] }
toml = { version = "0.8.12" }
toml_edit = { version = "0.22.13" }
toml_edit = { version = "0.22.13", features = ["serde"] }
tracing = { version = "0.1.40" }
tracing-durations-export = { version = "0.3.0", features = ["plot"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry"] }
Expand Down
1 change: 1 addition & 0 deletions crates/pypi-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ rkyv = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }

Expand Down
28 changes: 25 additions & 3 deletions crates/pypi-types/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::str::FromStr;
use indexmap::IndexMap;
use itertools::Itertools;
use mailparse::{MailHeaderMap, MailParseError};
use serde::de::IntoDeserializer;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::warn;
Expand Down Expand Up @@ -44,8 +45,12 @@ pub struct Metadata23 {
pub enum MetadataError {
#[error(transparent)]
MailParse(#[from] MailParseError),
#[error("Invalid `pyproject.toml`")]
InvalidPyprojectTomlSyntax(#[source] toml_edit::TomlError),
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.name` is not set.")]
InvalidPyprojectTomlMissingName(#[source] toml_edit::de::Error),
#[error(transparent)]
Toml(#[from] toml::de::Error),
InvalidPyprojectTomlSchema(toml_edit::de::Error),
#[error("metadata field {0} not found")]
FieldNotFound(&'static str),
#[error("invalid version: {0}")]
Expand Down Expand Up @@ -196,7 +201,7 @@ impl Metadata23 {

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

let project = pyproject_toml
.project
Expand Down Expand Up @@ -279,6 +284,23 @@ struct PyProjectToml {
tool: Option<Tool>,
}

impl PyProjectToml {
fn from_toml(toml: &str) -> Result<Self, MetadataError> {
let pyproject_toml: toml_edit::ImDocument<_> = toml_edit::ImDocument::from_str(toml)
.map_err(MetadataError::InvalidPyprojectTomlSyntax)?;
let pyproject_toml: Self = PyProjectToml::deserialize(pyproject_toml.into_deserializer())
.map_err(|err| {
// TODO(konsti): A typed error would be nicer, this can break on toml upgrades.
if err.message().contains("missing field `name`") {
MetadataError::InvalidPyprojectTomlMissingName(err)
} else {
MetadataError::InvalidPyprojectTomlSchema(err)
}
})?;
Ok(pyproject_toml)
}
}

/// PEP 621 project metadata.
///
/// This is a subset of the full metadata specification, and only includes the fields that are
Expand Down Expand Up @@ -435,7 +457,7 @@ pub struct RequiresDist {
impl RequiresDist {
/// Extract the [`RequiresDist`] from a `pyproject.toml` file, as specified in PEP 621.
pub fn parse_pyproject_toml(contents: &str) -> Result<Self, MetadataError> {
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
let pyproject_toml = PyProjectToml::from_toml(contents)?;

let project = pyproject_toml
.project
Expand Down
1 change: 1 addition & 0 deletions crates/uv-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }

Expand Down
12 changes: 9 additions & 3 deletions crates/uv-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use indoc::formatdoc;
use itertools::Itertools;
use regex::Regex;
use rustc_hash::FxHashMap;
use serde::de::{value, SeqAccess, Visitor};
use serde::de::{value, IntoDeserializer, SeqAccess, Visitor};
use serde::{de, Deserialize, Deserializer};
use std::ffi::OsString;
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -82,7 +82,9 @@ pub enum Error {
#[error("Invalid source distribution: {0}")]
InvalidSourceDist(String),
#[error("Invalid `pyproject.toml`")]
InvalidPyprojectToml(#[from] toml::de::Error),
InvalidPyprojectTomlSyntax(#[from] toml_edit::TomlError),
#[error("`pyproject.toml` does not match required schema. Note: When using any `[project]` field, at least `project.name` needs to be set.")]
InvalidPyprojectTomlSchema(#[from] toml_edit::de::Error),
#[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")]
EditableSetupPy,
#[error("Failed to install requirements from {0}")]
Expand Down Expand Up @@ -563,8 +565,12 @@ impl SourceBuild {
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
match fs::read_to_string(source_tree.join("pyproject.toml")) {
Ok(toml) => {
let pyproject_toml: toml_edit::ImDocument<_> =
toml_edit::ImDocument::from_str(&toml)
.map_err(Error::InvalidPyprojectTomlSyntax)?;
let pyproject_toml: PyProjectToml =
toml::from_str(&toml).map_err(Error::InvalidPyprojectToml)?;
PyProjectToml::deserialize(pyproject_toml.into_deserializer())
.map_err(Error::InvalidPyprojectTomlSchema)?;
let backend = if let Some(build_system) = pyproject_toml.build_system {
Pep517Backend {
// If `build-backend` is missing, inject the legacy setuptools backend, but
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ pathdiff = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true, features = ["serde"] }
toml_edit = { workspace = true }
tracing = { workspace = true }
47 changes: 47 additions & 0 deletions crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11810,3 +11810,50 @@ fn lock_implicit_virtual_path() -> Result<()> {

Ok(())
}

#[test]
fn invalid_project_table_dep() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("a/pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "a"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["b"]
[tool.uv.sources]
b = { path = "../b" }
"#,
)?;

let pyproject_toml = context.temp_dir.child("b/pyproject.toml");
pyproject_toml.write_str(
r"
[project.urls]
repository = 'https://github.com/octocat/octocat-python'
",
)?;

uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("a")), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
error: Failed to build: `b @ file://[TEMP_DIR]/b`
Caused by: Failed to extract static metadata from `pyproject.toml`
Caused by: `pyproject.toml` is using the `[project]` table, but the required `project.name` is not set.
Caused by: TOML parse error at line 2, column 10
|
2 | [project.urls]
| ^^^^^^^
missing field `name`
"###);

Ok(())
}

0 comments on commit f7f010d

Please sign in to comment.