mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Read package metadata from pyproject.toml
when statically defined (#2676)
## Summary Now that we're resolving metadata more aggressively for local sources, it's worth doing this. We now pull metadata from the `pyproject.toml` directly if it's statically-defined. Closes https://github.com/astral-sh/uv/issues/2629.
This commit is contained in:
parent
248d6f89ef
commit
365c292525
9 changed files with 302 additions and 34 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2750,6 +2750,7 @@ name = "pypi-types"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"indexmap",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
|
@ -2758,6 +2759,7 @@ dependencies = [
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
|
|
|
@ -471,6 +471,32 @@ impl Requirement {
|
||||||
(true, Vec::new())
|
(true, Vec::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the requirement with an additional marker added, to require the given extra.
|
||||||
|
///
|
||||||
|
/// For example, given `flask >= 2.0.2`, calling `with_extra_marker("dotenv")` would return
|
||||||
|
/// `flask >= 2.0.2 ; extra == "dotenv"`.
|
||||||
|
pub fn with_extra_marker(self, extra: &ExtraName) -> Self {
|
||||||
|
let marker = match self.marker {
|
||||||
|
Some(expression) => MarkerTree::And(vec![
|
||||||
|
expression,
|
||||||
|
MarkerTree::Expression(MarkerExpression {
|
||||||
|
l_value: MarkerValue::Extra,
|
||||||
|
operator: MarkerOperator::Equal,
|
||||||
|
r_value: MarkerValue::QuotedString(extra.to_string()),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
None => MarkerTree::Expression(MarkerExpression {
|
||||||
|
l_value: MarkerValue::Extra,
|
||||||
|
operator: MarkerOperator::Equal,
|
||||||
|
r_value: MarkerValue::QuotedString(extra.to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
marker: Some(marker),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnnamedRequirement {
|
impl UnnamedRequirement {
|
||||||
|
@ -1560,7 +1586,7 @@ mod tests {
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
use pep440_rs::{Operator, Version, VersionPattern, VersionSpecifier};
|
use pep440_rs::{Operator, Version, VersionPattern, VersionSpecifier};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||||
|
|
||||||
use crate::marker::{
|
use crate::marker::{
|
||||||
parse_markers_impl, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue,
|
parse_markers_impl, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue,
|
||||||
|
@ -2264,4 +2290,30 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_extra_marker() -> Result<(), InvalidNameError> {
|
||||||
|
let requirement = Requirement::from_str("pytest").unwrap();
|
||||||
|
let expected = Requirement::from_str("pytest; extra == 'dotenv'").unwrap();
|
||||||
|
let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
let requirement = Requirement::from_str("pytest; '4.0' >= python_version").unwrap();
|
||||||
|
let expected =
|
||||||
|
Requirement::from_str("pytest; '4.0' >= python_version and extra == 'dotenv'").unwrap();
|
||||||
|
let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
let requirement =
|
||||||
|
Requirement::from_str("pytest; '4.0' >= python_version or sys_platform == 'win32'")
|
||||||
|
.unwrap();
|
||||||
|
let expected = Requirement::from_str(
|
||||||
|
"pytest; ('4.0' >= python_version or sys_platform == 'win32') and extra == 'dotenv'",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,14 @@ pep508_rs = { workspace = true, features = ["rkyv", "serde"] }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
|
indexmap = { workspace = true, features = ["serde"] }
|
||||||
mailparse = { workspace = true }
|
mailparse = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! Derived from `pypi_types_crate`.
|
//! Derived from `pypi_types_crate`.
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -22,11 +23,10 @@ use crate::LenientVersionSpecifiers;
|
||||||
/// fields that are relevant to dependency resolution.
|
/// fields that are relevant to dependency resolution.
|
||||||
///
|
///
|
||||||
/// At present, we support up to version 2.3 of the metadata specification.
|
/// At present, we support up to version 2.3 of the metadata specification.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Metadata23 {
|
pub struct Metadata23 {
|
||||||
// Mandatory fields
|
// Mandatory fields
|
||||||
pub metadata_version: String,
|
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
// Optional fields
|
// Optional fields
|
||||||
|
@ -46,6 +46,9 @@ pub enum Error {
|
||||||
/// mail parse error
|
/// mail parse error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
MailParse(#[from] MailParseError),
|
MailParse(#[from] MailParseError),
|
||||||
|
/// TOML parse error
|
||||||
|
#[error(transparent)]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
/// Metadata field not found
|
/// Metadata field not found
|
||||||
#[error("metadata field {0} not found")]
|
#[error("metadata field {0} not found")]
|
||||||
FieldNotFound(&'static str),
|
FieldNotFound(&'static str),
|
||||||
|
@ -86,9 +89,6 @@ impl Metadata23 {
|
||||||
pub fn parse_metadata(content: &[u8]) -> Result<Self, Error> {
|
pub fn parse_metadata(content: &[u8]) -> Result<Self, Error> {
|
||||||
let headers = Headers::parse(content)?;
|
let headers = Headers::parse(content)?;
|
||||||
|
|
||||||
let metadata_version = headers
|
|
||||||
.get_first_value("Metadata-Version")
|
|
||||||
.ok_or(Error::FieldNotFound("Metadata-Version"))?;
|
|
||||||
let name = PackageName::new(
|
let name = PackageName::new(
|
||||||
headers
|
headers
|
||||||
.get_first_value("Name")
|
.get_first_value("Name")
|
||||||
|
@ -124,7 +124,6 @@ impl Metadata23 {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
metadata_version,
|
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
requires_dist,
|
requires_dist,
|
||||||
|
@ -200,7 +199,62 @@ impl Metadata23 {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
metadata_version,
|
name,
|
||||||
|
version,
|
||||||
|
requires_dist,
|
||||||
|
requires_python,
|
||||||
|
provides_extras,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the metadata from a `pyproject.toml` file, as specified in PEP 621.
|
||||||
|
pub fn parse_pyproject_toml(contents: &str) -> Result<Self, Error> {
|
||||||
|
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
|
||||||
|
|
||||||
|
let project = pyproject_toml
|
||||||
|
.project
|
||||||
|
.ok_or(Error::FieldNotFound("project"))?;
|
||||||
|
|
||||||
|
// If any of the fields we need were declared as dynamic, we can't use the `pyproject.toml` file.
|
||||||
|
let dynamic = project.dynamic.unwrap_or_default();
|
||||||
|
for field in dynamic {
|
||||||
|
match field.as_str() {
|
||||||
|
"dependencies" => return Err(Error::DynamicField("dependencies")),
|
||||||
|
"optional-dependencies" => {
|
||||||
|
return Err(Error::DynamicField("optional-dependencies"))
|
||||||
|
}
|
||||||
|
"requires-python" => return Err(Error::DynamicField("requires-python")),
|
||||||
|
"version" => return Err(Error::DynamicField("version")),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = project.name;
|
||||||
|
let version = project.version.ok_or(Error::FieldNotFound("version"))?;
|
||||||
|
let requires_python = project.requires_python.map(VersionSpecifiers::from);
|
||||||
|
|
||||||
|
// Extract the requirements.
|
||||||
|
let mut requires_dist = project
|
||||||
|
.dependencies
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(Requirement::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Extract the optional dependencies.
|
||||||
|
let mut provides_extras: Vec<ExtraName> = Vec::new();
|
||||||
|
for (extra, requirements) in project.optional_dependencies.unwrap_or_default() {
|
||||||
|
requires_dist.extend(
|
||||||
|
requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(Requirement::from)
|
||||||
|
.map(|requirement| requirement.with_extra_marker(&extra))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
provides_extras.push(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
requires_dist,
|
requires_dist,
|
||||||
|
@ -210,12 +264,44 @@ impl Metadata23 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub(crate) struct PyProjectToml {
|
||||||
|
/// Project metadata
|
||||||
|
pub(crate) project: Option<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PEP 621 project metadata.
|
||||||
|
///
|
||||||
|
/// This is a subset of the full metadata specification, and only includes the fields that are
|
||||||
|
/// relevant for dependency resolution.
|
||||||
|
///
|
||||||
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub(crate) struct Project {
|
||||||
|
/// The name of the project
|
||||||
|
pub(crate) name: PackageName,
|
||||||
|
/// The version of the project as supported by PEP 440
|
||||||
|
pub(crate) version: Option<Version>,
|
||||||
|
/// The Python version requirements of the project
|
||||||
|
pub(crate) requires_python: Option<LenientVersionSpecifiers>,
|
||||||
|
/// Project dependencies
|
||||||
|
pub(crate) dependencies: Option<Vec<LenientRequirement>>,
|
||||||
|
/// Optional dependencies
|
||||||
|
pub(crate) optional_dependencies: Option<IndexMap<ExtraName, Vec<LenientRequirement>>>,
|
||||||
|
/// Specifies which fields listed by PEP 621 were intentionally unspecified
|
||||||
|
/// so another tool can/will provide such metadata dynamically.
|
||||||
|
pub(crate) dynamic: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Python Package Metadata 1.0 and later as specified in
|
/// Python Package Metadata 1.0 and later as specified in
|
||||||
/// <https://peps.python.org/pep-0241/>.
|
/// <https://peps.python.org/pep-0241/>.
|
||||||
///
|
///
|
||||||
/// This is a subset of the full metadata specification, and only includes the
|
/// This is a subset of the full metadata specification, and only includes the
|
||||||
/// fields that have been consistent across all versions of the specification.
|
/// fields that have been consistent across all versions of the specification.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Metadata10 {
|
pub struct Metadata10 {
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
|
@ -303,19 +389,16 @@ mod tests {
|
||||||
|
|
||||||
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
|
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
|
||||||
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
||||||
assert_eq!(meta.metadata_version, "1.0");
|
|
||||||
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
assert_eq!(meta.version, Version::new([1, 0]));
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
|
||||||
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
|
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
|
||||||
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
||||||
assert_eq!(meta.metadata_version, "1.0");
|
|
||||||
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
assert_eq!(meta.version, Version::new([1, 0]));
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
|
||||||
let s = "Metadata-Version: 1.0\nName: =?utf-8?q?foobar?=\nVersion: 1.0";
|
let s = "Metadata-Version: 1.0\nName: =?utf-8?q?foobar?=\nVersion: 1.0";
|
||||||
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
|
||||||
assert_eq!(meta.metadata_version, "1.0");
|
|
||||||
assert_eq!(meta.name, PackageName::from_str("foobar").unwrap());
|
assert_eq!(meta.name, PackageName::from_str("foobar").unwrap());
|
||||||
assert_eq!(meta.version, Version::new([1, 0]));
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
|
||||||
|
@ -340,7 +423,6 @@ mod tests {
|
||||||
|
|
||||||
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0";
|
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0";
|
||||||
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
|
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
|
||||||
assert_eq!(meta.metadata_version, "2.3");
|
|
||||||
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
assert_eq!(meta.version, Version::new([1, 0]));
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
|
||||||
|
@ -350,9 +432,88 @@ mod tests {
|
||||||
|
|
||||||
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo";
|
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo";
|
||||||
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
|
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
|
||||||
assert_eq!(meta.metadata_version, "2.3");
|
|
||||||
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
assert_eq!(meta.version, Version::new([1, 0]));
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]);
|
assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_pyproject_toml() {
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s);
|
||||||
|
assert!(matches!(meta, Err(Error::FieldNotFound("version"))));
|
||||||
|
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
dynamic = ["version"]
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s);
|
||||||
|
assert!(matches!(meta, Err(Error::DynamicField("version"))));
|
||||||
|
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
version = "1.0"
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s).unwrap();
|
||||||
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
assert!(meta.requires_python.is_none());
|
||||||
|
assert!(meta.requires_dist.is_empty());
|
||||||
|
assert!(meta.provides_extras.is_empty());
|
||||||
|
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
version = "1.0"
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s).unwrap();
|
||||||
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap()));
|
||||||
|
assert!(meta.requires_dist.is_empty());
|
||||||
|
assert!(meta.provides_extras.is_empty());
|
||||||
|
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
version = "1.0"
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
dependencies = ["foo"]
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s).unwrap();
|
||||||
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap()));
|
||||||
|
assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]);
|
||||||
|
assert!(meta.provides_extras.is_empty());
|
||||||
|
|
||||||
|
let s = r#"
|
||||||
|
[project]
|
||||||
|
name = "asdf"
|
||||||
|
version = "1.0"
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
dependencies = ["foo"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dotenv = ["bar"]
|
||||||
|
"#;
|
||||||
|
let meta = Metadata23::parse_pyproject_toml(s).unwrap();
|
||||||
|
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
|
||||||
|
assert_eq!(meta.version, Version::new([1, 0]));
|
||||||
|
assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap()));
|
||||||
|
assert_eq!(
|
||||||
|
meta.requires_dist,
|
||||||
|
vec![
|
||||||
|
"foo".parse().unwrap(),
|
||||||
|
"bar; extra == \"dotenv\"".parse().unwrap()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(meta.provides_extras, vec!["dotenv".parse().unwrap()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::de::{value, SeqAccess, Visitor};
|
use serde::de::{value, SeqAccess, Visitor};
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
use serde::{de, Deserialize, Deserializer};
|
||||||
use tempfile::{tempdir_in, TempDir};
|
use tempfile::{tempdir_in, TempDir};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
@ -26,7 +26,7 @@ use tokio::sync::Mutex;
|
||||||
use tracing::{debug, info_span, instrument, Instrument};
|
use tracing::{debug, info_span, instrument, Instrument};
|
||||||
|
|
||||||
use distribution_types::Resolution;
|
use distribution_types::Resolution;
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{PackageName, Requirement};
|
use pep508_rs::{PackageName, Requirement};
|
||||||
use uv_fs::{PythonExt, Simplified};
|
use uv_fs::{PythonExt, Simplified};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
@ -193,8 +193,8 @@ impl Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pyproject.toml as specified in PEP 517.
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct PyProjectToml {
|
pub struct PyProjectToml {
|
||||||
/// Build-related data
|
/// Build-related data
|
||||||
|
@ -204,22 +204,23 @@ pub struct PyProjectToml {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `[project]` section of a pyproject.toml as specified in PEP 621.
|
/// The `[project]` section of a pyproject.toml as specified in PEP 621.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
///
|
||||||
|
/// This representation only includes a subset of the fields defined in PEP 621 necessary for
|
||||||
|
/// informing wheel builds.
|
||||||
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
/// The name of the project
|
/// The name of the project
|
||||||
pub name: PackageName,
|
pub name: PackageName,
|
||||||
/// The version of the project as supported by PEP 440
|
/// The version of the project as supported by PEP 440
|
||||||
pub version: Option<Version>,
|
pub version: Option<Version>,
|
||||||
/// The Python version requirements of the project
|
|
||||||
pub requires_python: Option<VersionSpecifiers>,
|
|
||||||
/// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool
|
/// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool
|
||||||
/// can/will provide such metadata dynamically.
|
/// can/will provide such metadata dynamically.
|
||||||
pub dynamic: Option<Vec<String>>,
|
pub dynamic: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
|
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct BuildSystem {
|
pub struct BuildSystem {
|
||||||
/// PEP 508 dependencies required to execute the build system.
|
/// PEP 508 dependencies required to execute the build system.
|
||||||
|
@ -237,8 +238,7 @@ impl BackendPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct BackendPath(Vec<String>);
|
pub struct BackendPath(Vec<String>);
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for BackendPath {
|
impl<'de> Deserialize<'de> for BackendPath {
|
||||||
|
|
|
@ -61,8 +61,12 @@ pub enum Error {
|
||||||
NotFound(PathBuf),
|
NotFound(PathBuf),
|
||||||
#[error("The source distribution is missing a `PKG-INFO` file")]
|
#[error("The source distribution is missing a `PKG-INFO` file")]
|
||||||
MissingPkgInfo,
|
MissingPkgInfo,
|
||||||
#[error("The source distribution does not support static metadata")]
|
#[error("The source distribution does not support static metadata in `PKG-INFO`")]
|
||||||
DynamicPkgInfo(#[source] pypi_types::Error),
|
DynamicPkgInfo(#[source] pypi_types::Error),
|
||||||
|
#[error("The source distribution is missing a `pyproject.toml` file")]
|
||||||
|
MissingPyprojectToml,
|
||||||
|
#[error("The source distribution does not support static metadata in `pyproject.toml`")]
|
||||||
|
DynamicPyprojectToml(#[source] pypi_types::Error),
|
||||||
#[error("Unsupported scheme in URL: {0}")]
|
#[error("Unsupported scheme in URL: {0}")]
|
||||||
UnsupportedScheme(String),
|
UnsupportedScheme(String),
|
||||||
|
|
||||||
|
|
|
@ -955,10 +955,10 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
|
||||||
) -> Result<Option<Metadata23>, Error> {
|
) -> Result<Option<Metadata23>, Error> {
|
||||||
debug!("Preparing metadata for: {source}");
|
debug!("Preparing metadata for: {source}");
|
||||||
|
|
||||||
// Attempt to read static metadata from the source distribution.
|
// Attempt to read static metadata from the `PKG-INFO` file.
|
||||||
match read_pkg_info(source_root).await {
|
match read_pkg_info(source_root).await {
|
||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
debug!("Found static metadata for: {source}");
|
debug!("Found static `PKG-INFO` for: {source}");
|
||||||
|
|
||||||
// Validate the metadata.
|
// Validate the metadata.
|
||||||
if let Some(name) = source.name() {
|
if let Some(name) = source.name() {
|
||||||
|
@ -973,7 +973,30 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
Err(err @ (Error::MissingPkgInfo | Error::DynamicPkgInfo(_))) => {
|
Err(err @ (Error::MissingPkgInfo | Error::DynamicPkgInfo(_))) => {
|
||||||
debug!("No static metadata available for: {source} ({err:?})");
|
debug!("No static `PKG-INFO` available for: {source} ({err:?})");
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to read static metadata from the `pyproject.toml`.
|
||||||
|
match read_pyproject_toml(source_root).await {
|
||||||
|
Ok(metadata) => {
|
||||||
|
debug!("Found static `pyproject.toml` for: {source}");
|
||||||
|
|
||||||
|
// Validate the metadata.
|
||||||
|
if let Some(name) = source.name() {
|
||||||
|
if metadata.name != *name {
|
||||||
|
return Err(Error::NameMismatch {
|
||||||
|
metadata: metadata.name,
|
||||||
|
given: name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Some(metadata));
|
||||||
|
}
|
||||||
|
Err(err @ (Error::MissingPyprojectToml | Error::DynamicPyprojectToml(_))) => {
|
||||||
|
debug!("No static `pyproject.toml` available for: {source} ({err:?})");
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
|
@ -1105,6 +1128,25 @@ pub(crate) async fn read_pkg_info(source_tree: &Path) -> Result<Metadata23, Erro
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the [`Metadata23`] from a source distribution's `pyproject.tom` file, if it defines static
|
||||||
|
/// metadata consistent with PEP 621.
|
||||||
|
pub(crate) async fn read_pyproject_toml(source_tree: &Path) -> Result<Metadata23, Error> {
|
||||||
|
// Read the `pyproject.toml` file.
|
||||||
|
let content = match fs::read_to_string(source_tree.join("pyproject.toml")).await {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
return Err(Error::MissingPyprojectToml);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(Error::CacheRead(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the metadata.
|
||||||
|
let metadata =
|
||||||
|
Metadata23::parse_pyproject_toml(&content).map_err(Error::DynamicPyprojectToml)?;
|
||||||
|
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read an existing HTTP-cached [`Manifest`], if it exists.
|
/// Read an existing HTTP-cached [`Manifest`], if it exists.
|
||||||
pub(crate) fn read_http_manifest(cache_entry: &CacheEntry) -> Result<Option<Manifest>, Error> {
|
pub(crate) fn read_http_manifest(cache_entry: &CacheEntry) -> Result<Option<Manifest>, Error> {
|
||||||
match fs_err::File::open(cache_entry.path()) {
|
match fs_err::File::open(cache_entry.path()) {
|
||||||
|
|
|
@ -4,11 +4,12 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use pep508_rs::Requirement;
|
use pep508_rs::Requirement;
|
||||||
|
use pypi_types::LenientRequirement;
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
use crate::ExtrasSpecification;
|
use crate::ExtrasSpecification;
|
||||||
|
|
||||||
/// A pyproject.toml as specified in PEP 517
|
/// A `pyproject.toml` as specified in PEP 517.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub(crate) struct PyProjectToml {
|
pub(crate) struct PyProjectToml {
|
||||||
|
@ -16,7 +17,12 @@ pub(crate) struct PyProjectToml {
|
||||||
pub(crate) project: Option<Project>,
|
pub(crate) project: Option<Project>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PEP 621 project metadata
|
/// PEP 621 project metadata.
|
||||||
|
///
|
||||||
|
/// This is a subset of the full metadata specification, and only includes the fields that are
|
||||||
|
/// relevant for extracting static requirements.
|
||||||
|
///
|
||||||
|
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub(crate) struct Project {
|
pub(crate) struct Project {
|
||||||
|
@ -80,7 +86,7 @@ impl Pep621Metadata {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.iter()
|
.iter()
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
.map(Requirement::from_str)
|
.map(|s| LenientRequirement::from_str(s).map(Requirement::from))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
// Include any optional dependencies specified in `extras`.
|
// Include any optional dependencies specified in `extras`.
|
||||||
|
@ -94,7 +100,7 @@ impl Pep621Metadata {
|
||||||
let requirements = requirements
|
let requirements = requirements
|
||||||
.iter()
|
.iter()
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
.map(Requirement::from_str)
|
.map(|s| LenientRequirement::from_str(s).map(Requirement::from))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
Ok::<(ExtraName, Vec<Requirement>), Pep621Error>((extra, requirements))
|
Ok::<(ExtraName, Vec<Requirement>), Pep621Error>((extra, requirements))
|
||||||
})
|
})
|
||||||
|
|
|
@ -881,7 +881,6 @@ fn install_editable_no_binary() {
|
||||||
fn reinstall_build_system() -> Result<()> {
|
fn reinstall_build_system() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
// Install devpi.
|
|
||||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||||
requirements_txt.write_str(indoc! {r"
|
requirements_txt.write_str(indoc! {r"
|
||||||
flit_core<4.0.0
|
flit_core<4.0.0
|
||||||
|
@ -900,7 +899,7 @@ fn reinstall_build_system() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 8 packages in [TIME]
|
Resolved 8 packages in [TIME]
|
||||||
Downloaded 7 packages in [TIME]
|
Downloaded 8 packages in [TIME]
|
||||||
Installed 8 packages in [TIME]
|
Installed 8 packages in [TIME]
|
||||||
+ blinker==1.7.0
|
+ blinker==1.7.0
|
||||||
+ click==8.1.7
|
+ click==8.1.7
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue