forked from astral-sh/uv
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fall back to PEP 517 hooks for non-compliant PEP 621 metadata (astral…
…-sh#2662) If you pass a `pyproject.toml` that use Hatch's context formatting API, we currently fail because the dependencies aren't valid under PEP 508. This PR makes the static metadata parsing a little more relaxed, so that we appropriately fall back to PEP 517 there.
- Loading branch information
1 parent
12846c2
commit 39769d8
Showing
9 changed files
with
347 additions
and
160 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
use indexmap::IndexMap; | ||
use rustc_hash::FxHashSet; | ||
use serde::{Deserialize, Serialize}; | ||
use std::str::FromStr; | ||
|
||
use pep508_rs::Requirement; | ||
use uv_normalize::{ExtraName, PackageName}; | ||
|
||
use crate::ExtrasSpecification; | ||
|
||
/// A pyproject.toml as specified in PEP 517 | ||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] | ||
#[serde(rename_all = "kebab-case")] | ||
pub(crate) struct PyProjectToml { | ||
/// Project metadata | ||
pub(crate) project: Option<Project>, | ||
} | ||
|
||
/// PEP 621 project metadata | ||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] | ||
#[serde(rename_all = "kebab-case")] | ||
pub(crate) struct Project { | ||
/// The name of the project | ||
pub(crate) name: PackageName, | ||
/// Project dependencies | ||
pub(crate) dependencies: Option<Vec<String>>, | ||
/// Optional dependencies | ||
pub(crate) optional_dependencies: Option<IndexMap<ExtraName, Vec<String>>>, | ||
/// 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>>, | ||
} | ||
|
||
/// The PEP 621 project metadata, with static requirements extracted in advance. | ||
#[derive(Debug)] | ||
pub(crate) struct Pep621Metadata { | ||
/// The name of the project. | ||
pub(crate) name: PackageName, | ||
/// The requirements extracted from the project. | ||
pub(crate) requirements: Vec<Requirement>, | ||
/// The extras used to collect requirements. | ||
pub(crate) used_extras: FxHashSet<ExtraName>, | ||
} | ||
|
||
#[derive(thiserror::Error, Debug)] | ||
pub(crate) enum Pep621Error { | ||
#[error(transparent)] | ||
Pep508(#[from] pep508_rs::Pep508Error), | ||
} | ||
|
||
impl Pep621Metadata { | ||
/// Extract the static [`Pep621Metadata`] from a [`Project`] and [`ExtrasSpecification`], if | ||
/// possible. | ||
/// | ||
/// If the project specifies dynamic dependencies, or if the project specifies dynamic optional | ||
/// dependencies and the extras are requested, the requirements cannot be extracted. | ||
/// | ||
/// Returns an error if the requirements are not valid PEP 508 requirements. | ||
pub(crate) fn try_from( | ||
project: Project, | ||
extras: &ExtrasSpecification, | ||
) -> Result<Option<Self>, Pep621Error> { | ||
if let Some(dynamic) = project.dynamic.as_ref() { | ||
// If the project specifies dynamic dependencies, we can't extract the requirements. | ||
if dynamic.iter().any(|field| field == "dependencies") { | ||
return Ok(None); | ||
} | ||
// If we requested extras, and the project specifies dynamic optional dependencies, we can't | ||
// extract the requirements. | ||
if !extras.is_empty() && dynamic.iter().any(|field| field == "optional-dependencies") { | ||
return Ok(None); | ||
} | ||
} | ||
|
||
let name = project.name; | ||
|
||
// Parse out the project requirements. | ||
let mut requirements = project | ||
.dependencies | ||
.unwrap_or_default() | ||
.iter() | ||
.map(String::as_str) | ||
.map(Requirement::from_str) | ||
.collect::<Result<Vec<_>, _>>()?; | ||
|
||
// Include any optional dependencies specified in `extras`. | ||
let mut used_extras = FxHashSet::default(); | ||
if !extras.is_empty() { | ||
if let Some(optional_dependencies) = project.optional_dependencies { | ||
// Parse out the optional dependencies. | ||
let optional_dependencies = optional_dependencies | ||
.into_iter() | ||
.map(|(extra, requirements)| { | ||
let requirements = requirements | ||
.iter() | ||
.map(String::as_str) | ||
.map(Requirement::from_str) | ||
.collect::<Result<Vec<_>, _>>()?; | ||
Ok::<(ExtraName, Vec<Requirement>), Pep621Error>((extra, requirements)) | ||
}) | ||
.collect::<Result<IndexMap<_, _>, _>>()?; | ||
|
||
// Include the optional dependencies if the extras are requested. | ||
for (extra, optional_requirements) in &optional_dependencies { | ||
if extras.contains(extra) { | ||
used_extras.insert(extra.clone()); | ||
requirements.extend(flatten_extra( | ||
&name, | ||
optional_requirements, | ||
&optional_dependencies, | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
Ok(Some(Self { | ||
name, | ||
requirements, | ||
used_extras, | ||
})) | ||
} | ||
} | ||
|
||
/// Given an extra in a project that may contain references to the project | ||
/// itself, flatten it into a list of requirements. | ||
/// | ||
/// For example: | ||
/// ```toml | ||
/// [project] | ||
/// name = "my-project" | ||
/// version = "0.0.1" | ||
/// dependencies = [ | ||
/// "tomli", | ||
/// ] | ||
/// | ||
/// [project.optional-dependencies] | ||
/// test = [ | ||
/// "pep517", | ||
/// ] | ||
/// dev = [ | ||
/// "my-project[test]", | ||
/// ] | ||
/// ``` | ||
fn flatten_extra( | ||
project_name: &PackageName, | ||
requirements: &[Requirement], | ||
extras: &IndexMap<ExtraName, Vec<Requirement>>, | ||
) -> Vec<Requirement> { | ||
fn inner( | ||
project_name: &PackageName, | ||
requirements: &[Requirement], | ||
extras: &IndexMap<ExtraName, Vec<Requirement>>, | ||
seen: &mut FxHashSet<ExtraName>, | ||
) -> Vec<Requirement> { | ||
let mut flattened = Vec::with_capacity(requirements.len()); | ||
for requirement in requirements { | ||
if requirement.name == *project_name { | ||
for extra in &requirement.extras { | ||
// Avoid infinite recursion on mutually recursive extras. | ||
if !seen.insert(extra.clone()) { | ||
continue; | ||
} | ||
|
||
// Flatten the extra requirements. | ||
for (other_extra, extra_requirements) in extras { | ||
if other_extra == extra { | ||
flattened.extend(inner(project_name, extra_requirements, extras, seen)); | ||
} | ||
} | ||
} | ||
} else { | ||
flattened.push(requirement.clone()); | ||
} | ||
} | ||
flattened | ||
} | ||
|
||
inner( | ||
project_name, | ||
requirements, | ||
extras, | ||
&mut FxHashSet::default(), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.