Skip to content

Commit

Permalink
refactor: add pixi_spec crate (#1741)
Browse files Browse the repository at this point in the history
Co-authored-by: Ruben Arts <ruben.arts@hotmail.com>
  • Loading branch information
baszalmstra and ruben-arts authored Aug 8, 2024
1 parent 4e12e86 commit 97d95c7
Show file tree
Hide file tree
Showing 35 changed files with 2,034 additions and 438 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ distribution-filename = { git = "https://github.com/astral-sh/uv", tag = "0.2.18
distribution-types = { git = "https://github.com/astral-sh/uv", tag = "0.2.18" }
dunce = "1.0.4"
fd-lock = "4.0.2"
file_url = "0.1.3"
flate2 = "1.0.28"
fs_extra = "1.3.0"
futures = "0.3.30"
Expand Down Expand Up @@ -92,6 +93,8 @@ tokio-util = "0.7.10"
toml_edit = "0.22.11"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
typed-path = "0.9.1"

# Bumping this to a higher version breaks the Windows path handling.
url = "2.5.0"
uv-auth = { git = "https://github.com/astral-sh/uv", tag = "0.2.18" }
Expand All @@ -117,6 +120,7 @@ pixi_consts = { path = "crates/pixi_consts" }
pixi_default_versions = { path = "crates/pixi_default_versions" }
pixi_manifest = { path = "crates/pixi_manifest" }
pixi_progress = { path = "crates/pixi_progress" }
pixi_spec = { path = "crates/pixi_spec" }
pixi_utils = { path = "crates/pixi_utils", default-features = false }
pixi_uv_conversions = { path = "crates/pixi_uv_conversions" }
pypi_mapping = { path = "crates/pypi_mapping" }
Expand Down Expand Up @@ -229,6 +233,7 @@ pixi_consts = { workspace = true }
pixi_default_versions = { workspace = true }
pixi_manifest = { workspace = true }
pixi_progress = { workspace = true }
pixi_spec = { workspace = true, features = ["toml_edit"] }
pixi_utils = { workspace = true, default-features = false }
pixi_uv_conversions = { workspace = true }
pypi_mapping = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions crates/pixi_manifest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lazy_static = { workspace = true }
pep440_rs = { workspace = true }
pep508_rs = { workspace = true }
pixi_consts = { workspace = true }
pixi_spec = { workspace = true, features = ["toml_edit"] }
regex = { workspace = true }
serde = { workspace = true }
serde-untagged = { workspace = true }
Expand Down
45 changes: 31 additions & 14 deletions crates/pixi_manifest/src/dependencies.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
use std::{borrow::Cow, hash::Hash};

use indexmap::{Equivalent, IndexMap, IndexSet};
use itertools::Either;
use pixi_spec::PixiSpec;
use rattler_conda_types::{MatchSpec, NamelessMatchSpec, PackageName};
use std::{borrow::Cow, hash::Hash};

use crate::{pypi::PyPiPackageName, PyPiRequirement};

pub type PyPiDependencies = Dependencies<PyPiPackageName, PyPiRequirement>;
pub type CondaDependencies = Dependencies<PackageName, NamelessMatchSpec>;
pub type CondaDependencies = Dependencies<PackageName, PixiSpec>;

/// Holds a list of dependencies where for each package name there can be multiple requirements.
/// Holds a list of dependencies where for each package name there can be
/// multiple requirements.
///
/// This is used when combining the dependencies of multiple features. Although each target can only
/// have one requirement for a given package, when combining the dependencies of multiple features
/// there can be multiple requirements for a given package.
/// This is used when combining the dependencies of multiple features. Although
/// each target can only have one requirement for a given package, when
/// combining the dependencies of multiple features there can be multiple
/// requirements for a given package.
///
/// The generic 'Dependencies' struct is aliased as specific PyPiDependencies and CondaDependencies struct to represent
/// Pypi and Conda dependencies respectively.
/// The generic 'Dependencies' struct is aliased as specific PyPiDependencies
/// and CondaDependencies struct to represent Pypi and Conda dependencies
/// respectively.
#[derive(Debug, Clone)]
pub struct Dependencies<N: Hash + Eq + Clone, D: Hash + Eq + Clone> {
Expand Down Expand Up @@ -43,7 +48,8 @@ impl<'a, M, N: Hash + Eq + Clone + 'a, D: Hash + Eq + Clone + 'a> From<M> for De
where
M: IntoIterator<Item = Cow<'a, IndexMap<N, D>>>,
{
/// Create Dependencies<N, D> from an iterator over items of type Cow<'a, IndexMap<N, D>
/// Create Dependencies<N, D> from an iterator over items of type Cow<'a,
/// IndexMap<N, D>
fn from(m: M) -> Self {
m.into_iter().fold(Self::default(), |mut acc: Self, deps| {
// Either clone the values from the Cow or move the values from the owned map.
Expand Down Expand Up @@ -85,8 +91,8 @@ impl<N: Hash + Eq + Clone, D: Hash + Eq + Clone> Dependencies<N, D> {
self.map.shift_remove_entry(name)
}

/// Combines two sets of dependencies where the requirements of `self` are overwritten if the
/// same package is also defined in `other`.
/// Combines two sets of dependencies where the requirements of `self` are
/// overwritten if the same package is also defined in `other`.
pub fn overwrite(&self, other: &Self) -> Self {
let mut map = self.map.clone();
for (name, specs) in other.map.iter() {
Expand All @@ -95,12 +101,14 @@ impl<N: Hash + Eq + Clone, D: Hash + Eq + Clone> Dependencies<N, D> {
Self { map }
}

/// Returns an iterator over tuples of dependency names and their combined requirements.
/// Returns an iterator over tuples of dependency names and their combined
/// requirements.
pub fn iter(&self) -> impl DoubleEndedIterator<Item = (&N, &IndexSet<D>)> + '_ {
self.map.iter()
}

/// Returns an iterator over tuples of dependency names and individual requirements.
/// Returns an iterator over tuples of dependency names and individual
/// requirements.
pub fn iter_specs(&self) -> impl DoubleEndedIterator<Item = (&N, &D)> + '_ {
self.map
.iter()
Expand All @@ -112,7 +120,8 @@ impl<N: Hash + Eq + Clone, D: Hash + Eq + Clone> Dependencies<N, D> {
self.map.keys()
}

/// Converts this instance into an iterator over tuples of dependency names and individual requirements.
/// Converts this instance into an iterator over tuples of dependency names
/// and individual requirements.
pub fn into_specs(self) -> impl DoubleEndedIterator<Item = (N, D)> {
self.map
.into_iter()
Expand All @@ -126,6 +135,14 @@ impl<N: Hash + Eq + Clone, D: Hash + Eq + Clone> Dependencies<N, D> {
{
self.map.contains_key(name)
}

/// Returns the package specs for the specified package name.
pub fn get<Q: ?Sized>(&self, name: &Q) -> Option<&IndexSet<D>>
where
Q: Hash + Equivalent<N>,
{
self.map.get(name)
}
}

impl Dependencies<PackageName, NamelessMatchSpec> {
Expand Down
111 changes: 15 additions & 96 deletions crates/pixi_manifest/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::fmt;

use rattler_conda_types::{ChannelConfig, NamelessMatchSpec, PackageName, Platform};
use toml_edit::{value, Array, InlineTable, Item, Table, Value};
use pixi_spec::PixiSpec;
use rattler_conda_types::{PackageName, Platform};
use toml_edit::{value, Array, Item, Table, Value};

use super::{consts, error::TomlError, pypi::PyPiPackageName, PyPiRequirement};
use crate::{consts::PYPROJECT_PIXI_PREFIX, FeatureName, SpecType, Task};
Expand Down Expand Up @@ -226,18 +227,14 @@ impl ManifestSource {
pub fn add_dependency(
&mut self,
name: &PackageName,
spec: &NamelessMatchSpec,
spec: &PixiSpec,
spec_type: SpecType,
platform: Option<Platform>,
feature_name: &FeatureName,
channel_config: &ChannelConfig,
) -> Result<(), TomlError> {
let dependency_table =
self.get_or_insert_toml_table(platform, feature_name, spec_type.name())?;
dependency_table.insert(
name.as_normalized(),
Item::Value(nameless_match_spec_to_toml(spec, channel_config)),
);
dependency_table.insert(name.as_normalized(), Item::Value(spec.to_toml_value()));
Ok(())
}

Expand Down Expand Up @@ -370,91 +367,9 @@ impl ManifestSource {
}
}

/// Given a nameless matchspec convert it into a TOML value. If the spec only
/// contains a version a string is returned, otherwise an entire table is
/// constructed.
fn nameless_match_spec_to_toml(spec: &NamelessMatchSpec, channel_config: &ChannelConfig) -> Value {
match spec {
NamelessMatchSpec {
version,
build: None,
build_number: None,
file_name: None,
channel: None,
subdir: None,
namespace: None,
md5: None,
sha256: None,
url: None,
} => {
// No other fields besides the version was specified, so we can just return the
// version as a string.
version
.as_ref()
.map_or_else(|| String::from("*"), |v| v.to_string())
.into()
}
NamelessMatchSpec {
version,
build,
build_number,
file_name,
channel,
subdir,
namespace,
md5,
sha256,
url,
} => {
let mut table = InlineTable::new();
table.insert(
"version",
version
.as_ref()
.map_or_else(|| String::from("*"), |v| v.to_string())
.into(),
);
if let Some(build) = build {
table.insert("build", build.to_string().into());
}
if let Some(build_number) = build_number {
table.insert("build_number", build_number.to_string().into());
}
if let Some(file_name) = file_name {
table.insert("file_name", file_name.to_string().into());
}
if let Some(channel) = channel {
table.insert(
"channel",
channel_config
.canonical_name(channel.base_url())
.as_str()
.into(),
);
}
if let Some(subdir) = subdir {
table.insert("subdir", subdir.to_string().into());
}
if let Some(namespace) = namespace {
table.insert("namespace", namespace.to_string().into());
}
if let Some(md5) = md5 {
table.insert("md5", format!("{:x}", md5).into());
}
if let Some(sha256) = sha256 {
table.insert("sha256", format!("{:x}", sha256).into());
}
if let Some(url) = url {
table.insert("url", url.to_string().into());
}
table.into()
}
}
}

#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::path::Path;

use insta::assert_snapshot;
use rattler_conda_types::{MatchSpec, ParseStrictness::Strict};
Expand All @@ -471,6 +386,12 @@ mod tests {
platforms = ["linux-64", "win-64", "osx-64"]
"#;

fn default_channel_config() -> rattler_conda_types::ChannelConfig {
rattler_conda_types::ChannelConfig::default_with_root_dir(
std::env::current_dir().expect("Could not retrieve the current directory"),
)
}

#[test]
fn test_nameless_to_toml() {
let examples = [
Expand All @@ -481,17 +402,15 @@ mod tests {
"rattler >=1 *cuda",
];

let channel_config = default_channel_config();
let mut table = toml_edit::DocumentMut::new();
let channel_config = ChannelConfig::default_with_root_dir(PathBuf::new());
for example in examples {
let spec = MatchSpec::from_str(example, Strict)
.unwrap()
.into_nameless()
.1;
table.insert(
example,
Item::Value(nameless_match_spec_to_toml(&spec, &channel_config)),
);
let spec = PixiSpec::from_nameless_matchspec(spec, &channel_config);
table.insert(example, Item::Value(spec.to_toml_value()));
}
assert_snapshot!(table);
}
Expand Down
6 changes: 3 additions & 3 deletions crates/pixi_manifest/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{borrow::Borrow, fmt::Display};

use itertools::Itertools;
use miette::{Diagnostic, IntoDiagnostic, LabeledSpan, NamedSource, Report};
use rattler_conda_types::{InvalidPackageNameError, ParseMatchSpecError};
use rattler_conda_types::{version_spec::ParseVersionSpecError, InvalidPackageNameError};
use thiserror::Error;

use super::pypi::pypi_requirement::Pep508ToPyPiRequirementError;
Expand All @@ -24,8 +24,8 @@ pub enum DependencyError {
pub enum RequirementConversionError {
#[error("Invalid package name error")]
InvalidPackageNameError(#[from] InvalidPackageNameError),
#[error("Failed to parse specification")]
ParseError(#[from] ParseMatchSpecError),
#[error("Failed to parse version")]
InvalidVersion(#[from] ParseVersionSpecError),
}

#[derive(Error, Debug, Clone, Diagnostic)]
Expand Down
Loading

0 comments on commit 97d95c7

Please sign in to comment.