Skip to content
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
3 changes: 2 additions & 1 deletion 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 @@ -148,7 +148,7 @@ tokio = { version = "1.35.1", features = ["fs", "io-util", "macros", "process",
tokio-stream = { version = "0.1.14" }
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
2 changes: 1 addition & 1 deletion crates/uv-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ serde_json = { workspace = true }
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
4 changes: 3 additions & 1 deletion crates/uv-build/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ pub enum Error {
#[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` are present in the directory", _0.simplified_display())]
InvalidSourceDist(PathBuf),
#[error("Invalid `pyproject.toml`")]
InvalidPyprojectToml(#[from] toml::de::Error),
InvalidPyprojectTomlSyntax(#[from] toml_edit::TomlError),
#[error("`pyproject.toml` does not match the required schema. When the `[project]` table is present, `project.name` must be present and non-empty.")]
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
8 changes: 6 additions & 2 deletions crates/uv-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use fs_err as fs;
use indoc::formatdoc;
use itertools::Itertools;
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::Formatter;
Expand Down Expand Up @@ -430,8 +430,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
9 changes: 6 additions & 3 deletions crates/uv-metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub enum Error {
#[error("The .dist-info directory name contains invalid characters")]
InvalidName(#[from] InvalidNameError),
#[error("The metadata at {0} is invalid")]
InvalidMetadata(String, pypi_types::MetadataError),
InvalidMetadata(String, Box<pypi_types::MetadataError>),
#[error("Failed to read from zip file")]
Zip(#[from] zip::result::ZipError),
#[error("Failed to read from zip file")]
Expand Down Expand Up @@ -285,7 +285,7 @@ pub async fn read_metadata_async_stream<R: futures::AsyncRead + Unpin>(
reader.read_to_end(&mut contents).await.unwrap();

let metadata = Metadata23::parse_metadata(&contents)
.map_err(|err| Error::InvalidMetadata(debug_path.to_string(), err))?;
.map_err(|err| Error::InvalidMetadata(debug_path.to_string(), Box::new(err)))?;
return Ok(metadata);
}

Expand All @@ -305,7 +305,10 @@ pub fn read_flat_wheel_metadata(
let dist_info_prefix = find_flat_dist_info(filename, &wheel)?;
let metadata = read_dist_info_metadata(&dist_info_prefix, &wheel)?;
Metadata23::parse_metadata(&metadata).map_err(|err| {
Error::InvalidMetadata(format!("{dist_info_prefix}.dist-info/METADATA"), err)
Error::InvalidMetadata(
format!("{dist_info_prefix}.dist-info/METADATA"),
Box::new(err),
)
})
}

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 @@ -12693,3 +12693,50 @@ fn lock_duplicate_sources() -> Result<()> {

Ok(())
}

#[test]
fn lock_invalid_project_table() -> 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(())
}
39 changes: 39 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1910,3 +1910,42 @@ fn run_exit_code() -> Result<()> {

Ok(())
}

#[test]
fn run_lock_invalid_project_table() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12", "3.11", "3.8"]);

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

[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#
})?;

let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;

uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 1, column 2
|
1 | [project.urls]
| ^^^^^^^
missing field `name`

"###);

Ok(())
}
Loading