Skip to content

Commit

Permalink
feat: add pinning strategy to the configuration (#1516)
Browse files Browse the repository at this point in the history
Improve UX for users that want a different pinning strategy in `pixi
add`.

With this pr users can set the strategy they want:
- `semver`: `1.2.3` -> `>=1.2.3, <2` and `0.1.0` -> `>=0.1.0, <0.2` and
`0.0.0` -> `>=0.0.0, <0.0.1`
- `no-pin`: `1.2.3` -> `*`
- `exact-version`: `1.2.3` -> `==1.2.3`
- `minor`: `1.2.3` -> `>=1.2.3, <1.3`
- `major`: `1.2.3` -> `>=1.2.3, <2`
- `latest-up`: `1.2.3` -> `>=1.2.3`

Users can set it using `pixi config set pinning-strategy no-pin`.

The `pixi add` will show what it added to the toml file:
```
> pixi config set pinning-strategy exact-version
> pixi add starship
Added starship ==1.19.0
```

Closes #1562
  • Loading branch information
ruben-arts authored Jul 19, 2024
1 parent 0159198 commit d161328
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 67 deletions.
8 changes: 8 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ pixi add --no-lockfile-update numpy
pixi add --feature featurex numpy
```

!!! tip
If you want to use a non default pinning strategy, you can set it using [pixi's configuration](reference/pixi_configuration#pinning-strategy).
```
pixi config set pinning-strategy no-pin --global
```
The default is `semver` which will pin the dependencies to the latest major version or minor for `v0` versions.


## `install`

Installs an environment based on the [manifest file](project_configuration.md).
Expand Down
15 changes: 15 additions & 0 deletions docs/reference/pixi_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ detached-environments = "/opt/pixi/envs"

```

### `pinning-strategy`
The strategy to use for pinning dependencies when running `pixi add`.
The default is `semver` but you can set the following:

- `no-pin`: No pinning, resulting in an unconstraint dependency. `*`
- `semver`: Pinning to the latest version that satisfies the semver constraint. Resulting in a pin to major for most versions and to minor for `v0` versions.
- `exact-version`: Pinning to the exact version, `1.2.3` -> `==1.2.3`.
- `major`: Pinning to the major version, `1.2.3` -> `>=1.2.3, <2`.
- `minor`: Pinning to the minor version, `1.2.3` -> `>=1.2.3, <1.3`.
- `latest-up`: Pinning to the latest version, `1.2.3` -> `>=1.2.3`.

```toml title="config.toml"
pinning-strategy = "no-pin"
```

### `mirrors`
Configuration for conda channel-mirrors, more info [below](#mirror-configuration).

Expand Down
55 changes: 10 additions & 45 deletions src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ use indexmap::IndexMap;
use itertools::Itertools;
use pep440_rs::VersionSpecifiers;
use pep508_rs::{Requirement, VersionOrUrl::VersionSpecifier};
use rattler_conda_types::{
version_spec::{LogicalOperator, RangeOperator},
MatchSpec, NamelessMatchSpec, PackageName, Platform, Version, VersionBumpType, VersionSpec,
};
use rattler_conda_types::{MatchSpec, NamelessMatchSpec, PackageName, Platform, Version};
use rattler_lock::{LockFile, Package};

use super::has_specs::HasSpecs;
Expand Down Expand Up @@ -398,9 +395,11 @@ fn update_pypi_specs_from_lock_file(
.flatten()
.collect_vec();

let pinning_strategy = project.config().pinning_strategy.unwrap_or_default();

// Determine the versions of the packages in the lock-file
for (name, _) in pypi_specs_to_add_constraints_for {
let version_constraint = determine_version_constraint(
let version_constraint = pinning_strategy.determine_version_constraint(
pypi_records
.iter()
.filter_map(|(data, _)| {
Expand Down Expand Up @@ -463,15 +462,18 @@ fn update_conda_specs_from_lock_file(
.flatten()
.collect_vec();

let pinning_strategy = project.config().pinning_strategy.unwrap_or_default();

for (name, (spec_type, _)) in conda_specs_to_add_constraints_for {
let version_constraint =
determine_version_constraint(conda_records.iter().filter_map(|record| {
let version_constraint = pinning_strategy.determine_version_constraint(
conda_records.iter().filter_map(|record| {
if record.package_record.name == name {
Some(record.package_record.version.version())
} else {
None
}
}));
}),
);

if let Some(version_constraint) = version_constraint {
implicit_constraints
Expand All @@ -495,27 +497,6 @@ fn update_conda_specs_from_lock_file(
Ok(implicit_constraints)
}

/// Given a set of versions, determines the best version constraint to use that
/// captures all of them.
fn determine_version_constraint<'a>(
versions: impl IntoIterator<Item = &'a Version>,
) -> Option<VersionSpec> {
let (min_version, max_version) = versions.into_iter().minmax().into_option()?;
let lower_bound = min_version.clone();
let upper_bound = max_version
.pop_segments(1)
.unwrap_or_else(|| max_version.clone())
.bump(VersionBumpType::Last)
.ok()?;
Some(VersionSpec::Group(
LogicalOperator::And,
vec![
VersionSpec::Range(RangeOperator::GreaterEquals, lower_bound),
VersionSpec::Range(RangeOperator::Less, upper_bound),
],
))
}

/// Constructs a new lock-file where some of the constraints have been removed.
fn unlock_packages(
project: &Project,
Expand All @@ -535,19 +516,3 @@ fn unlock_packages(
}
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_determine_version_constraint() {
insta::assert_snapshot!(determine_version_constraint(&["1.2.0".parse().unwrap()])
.unwrap()
.to_string(), @">=1.2.0,<1.3");

insta::assert_snapshot!(determine_version_constraint(&["1.2.0".parse().unwrap(), "1.3.0".parse().unwrap()])
.unwrap()
.to_string(), @">=1.2.0,<1.4");
}
}
Loading

0 comments on commit d161328

Please sign in to comment.