mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Show a concise error message for missing version field (#9912)
## Summary This now looks like: ``` 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 ``` Closes https://github.com/astral-sh/uv/issues/9910.
This commit is contained in:
parent
d4c2c46f6e
commit
48c9196f9e
6 changed files with 153 additions and 34 deletions
|
@ -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}")]
|
||||
|
|
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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))]
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -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"]
|
||||
"#,
|
||||
)?;
|
||||
|
|
|
@ -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(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue