Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract pypi-dependencies from environment #656

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
59 changes: 58 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ signal-hook = "0.3.17"

[dev-dependencies]
rattler_digest = "0.16.2"
rstest = "0.18.2"
serde_json = "1.0.111"
serial_test = "2.0.0"
tokio = { version = "1.35.1", features = ["rt"] }
Expand Down
3 changes: 2 additions & 1 deletion src/lock_file/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn resolve_dependencies<'p>(
platform: Platform,
conda_packages: &[RepoDataRecord],
) -> miette::Result<Vec<PinnedPackage<'p>>> {
let dependencies = project.pypi_dependencies(platform);
let dependencies = project.pypi_dependencies(Some(platform));
if dependencies.is_empty() {
return Ok(vec![]);
}
Expand Down Expand Up @@ -70,6 +70,7 @@ pub async fn resolve_dependencies<'p>(

let requirements = dependencies
.iter()
.flat_map(|(name, req)| req.iter().map(move |req| (name, req)))
.map(|(name, req)| req.as_pep508(name))
.collect::<Vec<pep508_rs::Requirement>>();

Expand Down
6 changes: 4 additions & 2 deletions src/lock_file/satisfiability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ pub fn lock_file_satisfies_project(
.collect::<Vec<_>>();

let mut pypi_dependencies = project
.pypi_dependencies(platform)
.pypi_dependencies(Some(platform))
.into_iter()
.map(|(name, requirement)| requirement.as_pep508(&name))
.flat_map(|(name, requirement)| {
requirement.into_iter().map(move |req| req.as_pep508(&name))
})
.map(DependencyKind::PyPi)
.peekable();

Expand Down
59 changes: 39 additions & 20 deletions src/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ use super::{
dependencies::Dependencies,
errors::{UnknownTask, UnsupportedPlatformError},
manifest::{self, EnvironmentName, Feature, FeatureName, SystemRequirements},
SpecType,
PyPiRequirement, SpecType,
};
use crate::{task::Task, Project};
use indexmap::IndexSet;
use indexmap::{IndexMap, IndexSet};
use itertools::Either;
use rattler_conda_types::{Channel, Platform};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fmt::Debug,
};
Expand Down Expand Up @@ -174,31 +176,48 @@ impl<'p> Environment<'p> {

/// Returns the dependencies to install for this environment.
///
/// The dependencies of all features are combined this means that if two features define a
/// The dependencies of all features are combined. This means that if two features define a
/// requirement for the same package that both requirements are returned. The different
/// requirements per package are sorted from the most specific feature/target to the least
/// specific.
/// requirements per package are sorted in the same order as the features they came from.
pub fn dependencies(&self, kind: Option<SpecType>, platform: Option<Platform>) -> Dependencies {
self.features()
.filter_map(|f| {
f.targets
.resolve(platform)
.rev()
.map(|t| t.dependencies(kind))
.fold(None, |acc: Option<Dependencies>, deps| {
Some(match acc {
None => Dependencies::from(deps.into_owned()),
Some(mut acc) => {
acc.extend_overwrite(deps.into_owned());
acc
}
})
})
})
.filter_map(|f| f.dependencies(kind, platform))
.map(|deps| Dependencies::from(deps.into_owned()))
.reduce(|acc, deps| acc.union(&deps))
.unwrap_or_default()
}

/// Returns the PyPi dependencies to install for this environment.
///
/// The dependencies of all features are combined. This means that if two features define a
/// requirement for the same package that both requirements are returned. The different
/// requirements per package are sorted in the same order as the features they came from.
pub fn pypi_dependencies(
&self,
platform: Option<Platform>,
) -> IndexMap<rip::types::PackageName, Vec<PyPiRequirement>> {
self.features()
.filter_map(|f| f.pypi_dependencies(platform))
.fold(IndexMap::default(), |mut acc, deps| {
// Either clone the values from the Cow or move the values from the owned map.
let deps_iter = match deps {
Cow::Borrowed(borrowed) => Either::Left(
borrowed
.into_iter()
.map(|(name, spec)| (name.clone(), spec.clone())),
),
Cow::Owned(owned) => Either::Right(owned.into_iter()),
};

// Add the requirements to the accumulator.
for (name, spec) in deps_iter {
acc.entry(name).or_default().push(spec);
}

acc
})
}

/// Validates that the given platform is supported by this environment.
fn validate_platform_support(
&self,
Expand Down
144 changes: 144 additions & 0 deletions src/project/manifest/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ use crate::project::SpecType;
use crate::task::Task;
use crate::utils::spanned::PixiSpanned;
use indexmap::IndexMap;
use itertools::Either;
use rattler_conda_types::{Channel, NamelessMatchSpec, PackageName, Platform};
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use serde_with::{serde_as, DisplayFromStr, PickFirst};
use std::borrow::Cow;
use std::collections::HashMap;

/// The name of a feature. This is either a string or default for the default feature.
Expand Down Expand Up @@ -70,6 +72,73 @@ pub struct Feature {
pub targets: Targets,
}

impl Feature {
/// Returns the dependencies of the feature for a given `spec_type` and `platform`.
///
/// This function returns a [`Cow`]. If the dependencies are not combined or overwritten by
/// multiple targets than this function returns a reference to the internal dependencies.
///
/// Returns `None` if this feature does not define any target that has any of the requested
/// dependencies.
pub fn dependencies(
&self,
spec_type: Option<SpecType>,
platform: Option<Platform>,
) -> Option<Cow<'_, IndexMap<PackageName, NamelessMatchSpec>>> {
self.targets
.resolve(platform)
// Get the targets in reverse order, from least specific to most specific.
// This is required because the extend function will overwrite existing keys.
.rev()
.filter_map(|t| t.dependencies(spec_type))
.filter(|deps| !deps.is_empty())
.fold(None, |acc, deps| match acc {
None => Some(deps),
Some(mut acc) => {
let deps_iter = match deps {
Cow::Borrowed(deps) => Either::Left(
deps.iter().map(|(name, spec)| (name.clone(), spec.clone())),
),
Cow::Owned(deps) => Either::Right(deps.into_iter()),
};

acc.to_mut().extend(deps_iter);
Some(acc)
}
})
}

/// Returns the PyPi dependencies of the feature for a given `platform`.
///
/// This function returns a [`Cow`]. If the dependencies are not combined or overwritten by
/// multiple targets than this function returns a reference to the internal dependencies.
///
/// Returns `None` if this feature does not define any target that has any of the requested
/// dependencies.
pub fn pypi_dependencies(
&self,
platform: Option<Platform>,
) -> Option<Cow<'_, IndexMap<rip::types::PackageName, PyPiRequirement>>> {
self.targets
.resolve(platform)
// Get the targets in reverse order, from least specific to most specific.
// This is required because the extend function will overwrite existing keys.
.rev()
.filter_map(|t| t.pypi_dependencies.as_ref())
.filter(|deps| !deps.is_empty())
.fold(None, |acc, deps| match acc {
None => Some(Cow::Borrowed(deps)),
Some(mut acc) => {
acc.to_mut().extend(
deps.into_iter()
.map(|(name, spec)| (name.clone(), spec.clone())),
);
Some(acc)
}
})
}
}

impl<'de> Deserialize<'de> for Feature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down Expand Up @@ -138,3 +207,78 @@ impl<'de> Deserialize<'de> for Feature {
})
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::project::manifest::Manifest;
use assert_matches::assert_matches;
use std::path::Path;

#[test]
fn test_dependencies_borrowed() {
let manifest = Manifest::from_str(
Path::new(""),
r#"
[project]
name = "foo"
platforms = ["linux-64", "osx-64", "win-64"]
channels = []

[dependencies]
foo = "1.0"

[host-dependencies]
foo = "2.0"

[feature.bla.dependencies]
foo = "2.0"

[feature.bla.host-dependencies]
# empty on purpose
"#,
)
.unwrap();

assert_matches!(
manifest
.default_feature()
.dependencies(Some(SpecType::Host), None)
.unwrap(),
Cow::Borrowed(_),
"[host-dependencies] should be borrowed"
);

assert_matches!(
manifest
.default_feature()
.dependencies(Some(SpecType::Run), None)
.unwrap(),
Cow::Borrowed(_),
"[dependencies] should be borrowed"
);

assert_matches!(
manifest.default_feature().dependencies(None, None).unwrap(),
Cow::Owned(_),
"combined dependencies should be owned"
);

let bla_feature = manifest
.parsed
.features
.get(&FeatureName::Named(String::from("bla")))
.unwrap();
assert_matches!(
bla_feature.dependencies(Some(SpecType::Run), None).unwrap(),
Cow::Borrowed(_),
"[feature.bla.dependencies] should be borrowed"
);

assert_matches!(
bla_feature.dependencies(None, None).unwrap(),
Cow::Borrowed(_),
"[feature.bla] combined dependencies should also be borrowed"
);
}
}
Loading
Loading