Skip to content

Commit d41c693

Browse files
Resolve optional dependencies recursively
1 parent d0146fb commit d41c693

File tree

5 files changed

+103
-105
lines changed

5 files changed

+103
-105
lines changed

Cargo.lock

+2-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pathdiff = "0.2.3"
6464
pep440_rs = "0.7.3"
6565
pep508_rs = "0.9.2"
6666
percent-encoding = "2.3.1"
67-
pyproject-toml = "0.13.4"
67+
pyproject-toml = { git = "https://github.com/olivier-lacroix/pyproject-toml-rs", rev = "a979a52db71200e2ce4bd7fc58f22c41e93ec183" }
6868
regex = "1.11.1"
6969
reqwest = { version = "0.12.12", default-features = false }
7070
reqwest-middleware = "0.4"

crates/pixi_manifest/src/pyproject.rs

+54-86
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use miette::{Diagnostic, IntoDiagnostic, Report, WrapErr};
99
use pep440_rs::{Version, VersionSpecifiers};
1010
use pep508_rs::Requirement;
1111
use pixi_spec::PixiSpec;
12-
use pyproject_toml::{self, pep735_resolve::Pep735Error, Contact};
12+
use pyproject_toml::{self, has_recursion::RecursionResolutionError, Contact};
1313
use rattler_conda_types::{PackageName, ParseStrictness::Lenient, VersionSpec};
1414
use thiserror::Error;
1515
use toml_span::Spanned;
@@ -22,7 +22,7 @@ use crate::{
2222
error::{DependencyError, GenericError},
2323
manifests::PackageManifest,
2424
toml::{
25-
pyproject::{TomlContact, TomlDependencyGroups, TomlProject},
25+
pyproject::{TomlContact, TomlDependencyGroups, TomlOptionalDependencies, TomlProject},
2626
ExternalPackageProperties, ExternalWorkspaceProperties, FromTomlStr, PyProjectToml,
2727
TomlManifest,
2828
},
@@ -97,11 +97,6 @@ impl PyProjectManifest {
9797
None
9898
}
9999

100-
/// Returns the project name as PEP508 name
101-
fn package_name(&self) -> Option<pep508_rs::PackageName> {
102-
pep508_rs::PackageName::new(self.name()?.to_string()).ok()
103-
}
104-
105100
fn tool(&self) -> Option<&Tool> {
106101
self.tool.as_ref()
107102
}
@@ -124,19 +119,18 @@ impl PyProjectManifest {
124119

125120
/// Returns optional dependencies from the `[project.optional-dependencies]`
126121
/// table
127-
fn optional_dependencies(&self) -> Option<IndexMap<String, Vec<Requirement>>> {
122+
fn optional_dependencies(
123+
&self,
124+
) -> Option<Result<IndexMap<String, Vec<Requirement>>, RecursionResolutionError>> {
128125
let project = self.project.project.as_ref()?;
129126
let optional_dependencies = project.optional_dependencies.as_ref()?;
130-
Some(
131-
optional_dependencies
132-
.iter()
133-
.map(|(k, v)| (k.clone(), v.iter().cloned().map(Spanned::take).collect()))
134-
.collect(),
135-
)
127+
Some(optional_dependencies.value.0.resolve())
136128
}
137129

138130
/// Returns dependency groups from the `[dependency-groups]` table
139-
fn dependency_groups(&self) -> Option<Result<IndexMap<String, Vec<Requirement>>, Pep735Error>> {
131+
fn dependency_groups(
132+
&self,
133+
) -> Option<Result<IndexMap<String, Vec<Requirement>>, RecursionResolutionError>> {
140134
let dg = self.project.dependency_groups.as_ref()?;
141135
Some(dg.value.0.resolve())
142136
}
@@ -145,37 +139,23 @@ impl PyProjectManifest {
145139
/// dependencies and/or dependency groups:
146140
/// - one environment is created per group with the same name
147141
/// - each environment includes the feature of the same name
148-
/// - it will also include other features inferred from any self references
149-
/// to other groups of optional dependencies (but won't for dependency
150-
/// groups, as recursion between groups is resolved upstream)
151-
pub fn environments_from_extras(&self) -> Result<HashMap<String, Vec<String>>, Pep735Error> {
142+
pub fn environments_from_dependency_groups(
143+
&self,
144+
) -> Result<HashMap<String, Vec<String>>, RecursionResolutionError> {
152145
let mut environments = HashMap::new();
153-
if let Some(extras) = self.optional_dependencies() {
154-
let pname = self.package_name();
155-
for (extra, reqs) in extras {
156-
let mut features = vec![extra.to_string()];
157-
// Add any references to other groups of extra dependencies
158-
for req in reqs.iter() {
159-
if pname.as_ref() == Some(&req.name) {
160-
for extra in &req.extras {
161-
features.push(extra.to_string())
162-
}
163-
}
164-
}
165-
// Environments can only contain number, strings and dashes
166-
environments.insert(extra.replace('_', "-").clone(), features);
167-
}
168-
}
169146

170-
if let Some(groups) = self.dependency_groups().transpose()? {
171-
for group in groups.into_keys() {
172-
let normalised = group.replace('_', "-");
173-
// Nothing to do if a group of optional dependencies has the same name as the
174-
// dependency group
175-
if !environments.contains_key(&normalised) {
176-
environments.insert(normalised.clone(), vec![normalised]);
177-
}
178-
}
147+
let groups = self
148+
.optional_dependencies()
149+
.transpose()?
150+
.unwrap_or_default()
151+
.into_iter()
152+
.chain(self.dependency_groups().transpose()?.unwrap_or_default());
153+
154+
for (group, _) in groups {
155+
let normalised = group.replace('_', "-");
156+
environments
157+
.entry(normalised.clone())
158+
.or_insert_with(|| vec![group]);
179159
}
180160

181161
Ok(environments)
@@ -187,7 +167,7 @@ pub enum PyProjectToManifestError {
187167
#[error("Unsupported pep508 requirement: '{0}'")]
188168
DependencyError(Requirement, #[source] DependencyError),
189169
#[error(transparent)]
190-
DependencyGroupError(#[from] Pep735Error),
170+
DependencyGroupError(#[from] RecursionResolutionError),
191171
#[error(transparent)]
192172
TomlError(#[from] TomlError),
193173
}
@@ -200,7 +180,7 @@ pub struct PyProjectFields {
200180
pub authors: Option<Vec<Spanned<TomlContact>>>,
201181
pub requires_python: Option<Spanned<VersionSpecifiers>>,
202182
pub dependencies: Option<Vec<Spanned<Requirement>>>,
203-
pub optional_dependencies: Option<IndexMap<String, Vec<Spanned<Requirement>>>>,
183+
pub optional_dependencies: Option<Spanned<TomlOptionalDependencies>>,
204184
}
205185

206186
impl From<TomlProject> for PyProjectFields {
@@ -391,12 +371,7 @@ impl PyProjectManifest {
391371
}
392372

393373
// For each group of optional dependency or dependency group, add pypi
394-
// dependencies, filtering out self-references in optional dependencies
395-
let project_name = workspace_manifest
396-
.workspace
397-
.name
398-
.clone()
399-
.and_then(|name| pep508_rs::PackageName::new(name).ok());
374+
// dependencies
400375
for (group, reqs) in pypi_dependency_groups {
401376
let feature_name = FeatureName::from(group.to_string());
402377
let target = workspace_manifest
@@ -406,16 +381,13 @@ impl PyProjectManifest {
406381
.targets
407382
.default_mut();
408383
for requirement in reqs.iter() {
409-
// filter out any self references in groups of extra dependencies
410-
if project_name.as_ref() != Some(&requirement.name) {
411-
target
412-
.try_add_pep508_dependency(
413-
requirement,
414-
None,
415-
DependencyOverwriteBehavior::Error,
416-
)
417-
.map_err(|err| GenericError::new(format!("{}", err)))?;
418-
}
384+
target
385+
.try_add_pep508_dependency(
386+
requirement,
387+
None,
388+
DependencyOverwriteBehavior::Error,
389+
)
390+
.map_err(|err| GenericError::new(format!("{}", err)))?;
419391
}
420392
}
421393

@@ -424,31 +396,27 @@ impl PyProjectManifest {
424396

425397
fn extract_dependency_groups(
426398
dependency_groups: Option<Spanned<TomlDependencyGroups>>,
427-
optional_dependencies: Option<IndexMap<String, Vec<Spanned<Requirement>>>>,
399+
optional_dependencies: Option<Spanned<TomlOptionalDependencies>>,
428400
) -> Result<Vec<(String, Vec<Requirement>)>, TomlError> {
429-
Ok(optional_dependencies
430-
.map(|deps| {
431-
deps.into_iter()
432-
.map(|(group, reqs)| {
433-
(
434-
group,
435-
reqs.into_iter().map(Spanned::take).collect::<Vec<_>>(),
436-
)
437-
})
438-
.collect()
439-
})
440-
.into_iter()
441-
.chain(
442-
dependency_groups
443-
.map(|Spanned { span, value }| {
444-
value.0.resolve().map_err(|err| {
445-
GenericError::new(format!("{}", err)).with_span(span.into())
446-
})
447-
})
448-
.transpose()?,
449-
)
450-
.flat_map(|map| map.into_iter())
451-
.collect::<Vec<_>>())
401+
let mut result = Vec::new();
402+
403+
if let Some(Spanned { span, value }) = optional_dependencies {
404+
let resolved = value
405+
.0
406+
.resolve()
407+
.map_err(|err| GenericError::new(err.to_string()).with_span(span.into()))?;
408+
result.extend(resolved);
409+
}
410+
411+
if let Some(Spanned { span, value }) = dependency_groups {
412+
let resolved = value
413+
.0
414+
.resolve()
415+
.map_err(|err| GenericError::new(err.to_string()).with_span(span.into()))?;
416+
result.extend(resolved);
417+
}
418+
419+
Ok(result)
452420
}
453421
}
454422

crates/pixi_manifest/src/toml/pyproject.rs

+43-14
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use pep440_rs::{Version, VersionSpecifiers};
88
use pep508_rs::Requirement;
99
use pixi_toml::{DeserializeAs, Same, TomlFromStr, TomlIndexMap, TomlWith};
1010
use pyproject_toml::{
11-
BuildSystem, Contact, DependencyGroupSpecifier, DependencyGroups, License, Project, ReadMe,
11+
BuildSystem, Contact, DependencyGroupSpecifier, DependencyGroups, License,
12+
OptionalDependencies, Project, ReadMe,
1213
};
1314
use toml_span::{
1415
de_helpers::{expected, TableHelper},
@@ -154,16 +155,17 @@ pub struct TomlProject {
154155
/// Project dependencies
155156
pub dependencies: Option<Vec<Spanned<Requirement>>>,
156157
/// Optional dependencies
157-
pub optional_dependencies: Option<IndexMap<String, Vec<Spanned<Requirement>>>>,
158+
pub optional_dependencies: Option<Spanned<TomlOptionalDependencies>>,
158159
/// Specifies which fields listed by PEP 621 were intentionally unspecified
159160
/// so another tool can/will provide such metadata dynamically.
160161
pub dynamic: Option<Vec<Spanned<String>>>,
161162
}
162163

163164
impl TomlProject {
164165
pub fn into_inner(self) -> Project {
166+
let name = self.name.take();
165167
Project {
166-
name: self.name.take(),
168+
name: name.clone(),
167169
version: self.version.map(Spanned::take),
168170
description: self.description.map(Spanned::take),
169171
readme: self.readme.map(Spanned::take).map(TomlReadme::into_inner),
@@ -213,12 +215,14 @@ impl TomlProject {
213215
dependencies: self
214216
.dependencies
215217
.map(|dependencies| dependencies.into_iter().map(Spanned::take).collect()),
216-
optional_dependencies: self.optional_dependencies.map(|optional_dependencies| {
217-
optional_dependencies
218-
.into_iter()
219-
.map(|(k, v)| (k, v.into_iter().map(Spanned::take).collect()))
220-
.collect()
221-
}),
218+
optional_dependencies: self
219+
.optional_dependencies
220+
.map(Spanned::take)
221+
.map(|x| x.into_inner())
222+
.map(|od| OptionalDependencies {
223+
inner: od.inner,
224+
self_reference_name: Some(name),
225+
}),
222226
dynamic: self
223227
.dynamic
224228
.map(|dynamic| dynamic.into_iter().map(Spanned::take).collect()),
@@ -262,11 +266,7 @@ impl<'de> toml_span::Deserialize<'de> for TomlProject {
262266
let dependencies = th
263267
.optional::<TomlWith<_, Vec<Spanned<TomlFromStr<_>>>>>("dependencies")
264268
.map(TomlWith::into_inner);
265-
let optional_dependencies = th
266-
.optional::<TomlWith<_, TomlIndexMap<_, Vec<Spanned<TomlFromStr<_>>>>>>(
267-
"optional-dependencies",
268-
)
269-
.map(TomlWith::into_inner);
269+
let optional_dependencies = th.optional("optional-dependencies");
270270
let dynamic = th.optional("dynamic");
271271

272272
th.finalize(None)?;
@@ -428,6 +428,35 @@ impl<'de> DeserializeAs<'de, Contact> for TomlContact {
428428
}
429429
}
430430

431+
/// A wrapper around [`OptionalDependencies`] that implements
432+
/// [`toml_span::Deserialize`] and [`pixi_toml::DeserializeAs`].
433+
#[derive(Debug)]
434+
pub struct TomlOptionalDependencies(pub OptionalDependencies);
435+
436+
impl TomlOptionalDependencies {
437+
pub fn into_inner(self) -> OptionalDependencies {
438+
self.0
439+
}
440+
}
441+
442+
impl<'de> toml_span::Deserialize<'de> for TomlOptionalDependencies {
443+
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
444+
Ok(Self(OptionalDependencies {
445+
inner: TomlWith::<_, TomlIndexMap<String, Vec<TomlFromStr<Requirement>>>>::deserialize(
446+
value,
447+
)?
448+
.into_inner(),
449+
self_reference_name: None,
450+
}))
451+
}
452+
}
453+
454+
impl<'de> DeserializeAs<'de, OptionalDependencies> for TomlOptionalDependencies {
455+
fn deserialize_as(value: &mut Value<'de>) -> Result<OptionalDependencies, DeserError> {
456+
Self::deserialize(value).map(Self::into_inner)
457+
}
458+
}
459+
431460
/// A wrapper around [`DependencyGroups`] that implements
432461
/// [`toml_span::Deserialize`] and [`pixi_toml::DeserializeAs`].
433462
#[derive(Debug)]

src/cli/init.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ pub async fn execute(args: Args) -> miette::Result<()> {
402402
Some(name) => (name, false),
403403
None => (default_name.as_str(), true),
404404
};
405-
let environments = pyproject.environments_from_extras().into_diagnostic()?;
405+
let environments = pyproject
406+
.environments_from_dependency_groups()
407+
.into_diagnostic()?;
406408
let rv = env
407409
.render_named_str(
408410
consts::PYPROJECT_MANIFEST,

0 commit comments

Comments
 (0)