mirror of https://github.com/astral-sh/uv
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
|
|
@ -34,10 +34,10 @@ pub enum MetadataError {
|
||||||
MailParse(#[from] MailParseError),
|
MailParse(#[from] MailParseError),
|
||||||
#[error("Invalid `pyproject.toml`")]
|
#[error("Invalid `pyproject.toml`")]
|
||||||
InvalidPyprojectTomlSyntax(#[source] toml_edit::TomlError),
|
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)]
|
#[error(transparent)]
|
||||||
InvalidPyprojectTomlSchema(toml_edit::de::Error),
|
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")]
|
#[error("Metadata field {0} not found")]
|
||||||
FieldNotFound(&'static str),
|
FieldNotFound(&'static str),
|
||||||
#[error("Invalid version: {0}")]
|
#[error("Invalid version: {0}")]
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
use crate::{
|
use std::str::FromStr;
|
||||||
LenientRequirement, LenientVersionSpecifiers, MetadataError, ResolutionMetadata,
|
|
||||||
VerbatimParsedUrl,
|
|
||||||
};
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::de::IntoDeserializer;
|
use serde::de::IntoDeserializer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_pep440::{Version, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifiers};
|
||||||
use uv_pep508::Requirement;
|
use uv_pep508::Requirement;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
LenientRequirement, LenientVersionSpecifiers, MetadataError, ResolutionMetadata,
|
||||||
|
VerbatimParsedUrl,
|
||||||
|
};
|
||||||
|
|
||||||
/// Extract the metadata from a `pyproject.toml` file, as specified in PEP 621.
|
/// 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
|
/// 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)
|
let pyproject_toml: toml_edit::ImDocument<_> = toml_edit::ImDocument::from_str(toml)
|
||||||
.map_err(MetadataError::InvalidPyprojectTomlSyntax)?;
|
.map_err(MetadataError::InvalidPyprojectTomlSyntax)?;
|
||||||
let pyproject_toml: Self = PyProjectToml::deserialize(pyproject_toml.into_deserializer())
|
let pyproject_toml: Self = PyProjectToml::deserialize(pyproject_toml.into_deserializer())
|
||||||
.map_err(|err| {
|
.map_err(MetadataError::InvalidPyprojectTomlSchema)?;
|
||||||
// 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)
|
Ok(pyproject_toml)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +127,7 @@ impl PyProjectToml {
|
||||||
///
|
///
|
||||||
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(try_from = "PyprojectTomlWire")]
|
||||||
struct Project {
|
struct Project {
|
||||||
/// The name of the project
|
/// The name of the project
|
||||||
name: PackageName,
|
name: PackageName,
|
||||||
|
|
@ -148,6 +144,33 @@ struct Project {
|
||||||
dynamic: Option<Vec<String>>,
|
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)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
struct Tool {
|
struct Tool {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ pub enum PyprojectTomlError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
TomlSchema(#[from] toml_edit::de::Error),
|
TomlSchema(#[from] toml_edit::de::Error),
|
||||||
#[error("`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set")]
|
#[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.
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
|
|
@ -63,15 +65,8 @@ impl PyProjectToml {
|
||||||
pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
|
pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
|
||||||
let pyproject: toml_edit::ImDocument<_> =
|
let pyproject: toml_edit::ImDocument<_> =
|
||||||
toml_edit::ImDocument::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
|
toml_edit::ImDocument::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
|
||||||
let pyproject =
|
let pyproject = PyProjectToml::deserialize(pyproject.into_deserializer())
|
||||||
PyProjectToml::deserialize(pyproject.into_deserializer()).map_err(|err| {
|
.map_err(PyprojectTomlError::TomlSchema)?;
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
Ok(PyProjectToml { raw, ..pyproject })
|
Ok(PyProjectToml { raw, ..pyproject })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +202,7 @@ impl<'de> Deserialize<'de> for DependencyGroupSpecifier {
|
||||||
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||||
#[cfg_attr(test, derive(Serialize))]
|
#[cfg_attr(test, derive(Serialize))]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
/// The name of the project
|
/// The name of the project
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
|
|
@ -228,6 +223,48 @@ pub struct Project {
|
||||||
pub(crate) scripts: Option<serde::de::IgnoredAny>,
|
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)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(test, derive(Serialize))]
|
#[cfg_attr(test, derive(Serialize))]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[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]
|
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
× Failed to build `b @ file://[TEMP_DIR]/b`
|
× Failed to build `b @ file://[TEMP_DIR]/b`
|
||||||
├─▶ Failed to extract static metadata from `pyproject.toml`
|
├─▶ 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
|
╰─▶ TOML parse error at line 2, column 10
|
||||||
|
|
|
|
||||||
2 | [project.urls]
|
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(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,7 @@ fn invalid_pyproject_toml_project_schema() -> Result<()> {
|
||||||
|
|
|
|
||||||
1 | [project]
|
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(
|
pyproject_toml.write_str(
|
||||||
r#"[project]
|
r#"[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = ["flask==1.0.x"]
|
dependencies = ["flask==1.0.x"]
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -2774,13 +2774,11 @@ fn run_invalid_project_table() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Failed to parse: `pyproject.toml`
|
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
|
Caused by: TOML parse error at line 1, column 2
|
||||||
|
|
|
|
||||||
1 | [project.urls]
|
1 | [project.urls]
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
missing field `name`
|
`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set
|
||||||
|
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue