Skip to content

Show a concise error message for missing version field #9912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/uv-pypi-types/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ pub enum MetadataError {
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` field is not set.")]
InvalidPyprojectTomlMissingName(#[source] toml_edit::de::Error),
#[error(transparent)]
InvalidPyprojectTomlSchema(toml_edit::de::Error),
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set")]
MissingName,
#[error("Metadata field {0} not found")]
FieldNotFound(&'static str),
#[error("Invalid version: {0}")]
Expand Down
51 changes: 37 additions & 14 deletions crates/uv-pypi-types/src/metadata/pyproject_toml.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use crate::{
LenientRequirement, LenientVersionSpecifiers, MetadataError, ResolutionMetadata,
VerbatimParsedUrl,
};
use std::str::FromStr;

use indexmap::IndexMap;
use itertools::Itertools;
use serde::de::IntoDeserializer;
use serde::{Deserialize, Serialize};
use std::str::FromStr;

use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::Requirement;

use crate::{
LenientRequirement, LenientVersionSpecifiers, MetadataError, ResolutionMetadata,
VerbatimParsedUrl,
};

/// Extract the metadata from a `pyproject.toml` file, as specified in PEP 621.
///
/// If we're coming from a source distribution, we may already know the version (unlike for a source
Expand Down Expand Up @@ -112,14 +115,7 @@ impl PyProjectToml {
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)
}
})?;
.map_err(MetadataError::InvalidPyprojectTomlSchema)?;
Ok(pyproject_toml)
}
}
Expand All @@ -131,7 +127,7 @@ impl PyProjectToml {
///
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[serde(try_from = "PyprojectTomlWire")]
struct Project {
/// The name of the project
name: PackageName,
Expand All @@ -148,6 +144,33 @@ struct Project {
dynamic: Option<Vec<String>>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PyprojectTomlWire {
name: Option<PackageName>,
version: Option<Version>,
requires_python: Option<String>,
dependencies: Option<Vec<String>>,
optional_dependencies: Option<IndexMap<ExtraName, Vec<String>>>,
dynamic: Option<Vec<String>>,
}

impl TryFrom<PyprojectTomlWire> for Project {
type Error = MetadataError;

fn try_from(wire: PyprojectTomlWire) -> Result<Self, Self::Error> {
let name = wire.name.ok_or(MetadataError::MissingName)?;
Ok(Project {
name,
version: wire.version,
requires_python: wire.requires_python,
dependencies: wire.dependencies,
optional_dependencies: wire.optional_dependencies,
dynamic: wire.dynamic,
})
}
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Tool {
Expand Down
59 changes: 48 additions & 11 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ pub enum PyprojectTomlError {
#[error(transparent)]
TomlSchema(#[from] toml_edit::de::Error),
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set")]
MissingName(#[source] toml_edit::de::Error),
MissingName,
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list")]
MissingVersion,
}

/// A `pyproject.toml` as specified in PEP 517.
Expand Down Expand Up @@ -63,15 +65,8 @@ impl PyProjectToml {
pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
let pyproject: toml_edit::ImDocument<_> =
toml_edit::ImDocument::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
let pyproject =
PyProjectToml::deserialize(pyproject.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`") {
PyprojectTomlError::MissingName(err)
} else {
PyprojectTomlError::TomlSchema(err)
}
})?;
let pyproject = PyProjectToml::deserialize(pyproject.into_deserializer())
.map_err(PyprojectTomlError::TomlSchema)?;
Ok(PyProjectToml { raw, ..pyproject })
}

Expand Down Expand Up @@ -207,7 +202,7 @@ impl<'de> Deserialize<'de> for DependencyGroupSpecifier {
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(Serialize))]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
pub struct Project {
/// The name of the project
pub name: PackageName,
Expand All @@ -228,6 +223,48 @@ pub struct Project {
pub(crate) scripts: Option<serde::de::IgnoredAny>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct ProjectWire {
name: Option<PackageName>,
version: Option<Version>,
dynamic: Option<Vec<String>>,
requires_python: Option<VersionSpecifiers>,
dependencies: Option<Vec<String>>,
optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
gui_scripts: Option<serde::de::IgnoredAny>,
scripts: Option<serde::de::IgnoredAny>,
}

impl TryFrom<ProjectWire> for Project {
type Error = PyprojectTomlError;

fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
// If `[project.name]` is not present, show a dedicated error message.
let name = value.name.ok_or(PyprojectTomlError::MissingName)?;

// If `[project.version]` is not present (or listed in `[project.dynamic]`), show a dedicated error message.
if value.version.is_none()
&& !value
.dynamic
.as_ref()
.is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
{
return Err(PyprojectTomlError::MissingVersion);
}

Ok(Project {
name,
version: value.version,
requires_python: value.requires_python,
dependencies: value.dependencies,
optional_dependencies: value.optional_dependencies,
gui_scripts: value.gui_scripts,
scripts: value.scripts,
})
}
}

#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand Down
65 changes: 63 additions & 2 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16778,12 +16778,73 @@ fn lock_invalid_project_table() -> Result<()> {
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
× Failed to build `b @ file://[TEMP_DIR]/b`
├─▶ Failed to extract static metadata from `pyproject.toml`
├─▶ `pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set.
╰─▶ TOML parse error at line 2, column 10
|
2 | [project.urls]
| ^^^^^^^
missing field `name`
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
"###);

Ok(())
}

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

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc::indoc! {
r#"
[project]
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
})?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
"###);

Ok(())
}

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

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc::indoc! {
r#"
[project]
name = "project"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
})?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list
"###);

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ fn invalid_pyproject_toml_project_schema() -> Result<()> {
|
1 | [project]
| ^^^^^^^^^
missing field `name`
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
"###
);

Expand Down Expand Up @@ -285,6 +284,7 @@ fn invalid_pyproject_toml_requirement_indirect() -> Result<()> {
pyproject_toml.write_str(
r#"[project]
name = "project"
version = "0.1.0"
dependencies = ["flask==1.0.x"]
"#,
)?;
Expand Down
4 changes: 1 addition & 3 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2774,13 +2774,11 @@ fn run_invalid_project_table() -> Result<()> {
----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: `pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
Caused by: TOML parse error at line 1, column 2
|
1 | [project.urls]
| ^^^^^^^
missing field `name`
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
"###);

Ok(())
Expand Down
Loading