From 0b816701694edd286d864d1451efd4866d55aec9 Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Fri, 18 Oct 2024 11:05:23 +0200 Subject: [PATCH] ux: add python as a minor pinning as its to important for beginner users to not get wrong --- src/cli/add.rs | 37 ++++++++++++++++----------- tests/add_tests.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/cli/add.rs b/src/cli/add.rs index 451443d3e..934393606 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -3,11 +3,21 @@ use std::{ str::FromStr, }; +use super::has_specs::HasSpecs; +use crate::environment::LockFileUsage; +use crate::{ + cli::cli_config::{DependencyConfig, PrefixUpdateConfig, ProjectConfig}, + environment::verify_prefix_location_unchanged, + load_lock_file, + lock_file::{filter_lock_file, LockFileDerivedData, UpdateContext}, + project::{grouped_environment::GroupedEnvironment, DependencyType, Project}, +}; use clap::Parser; use indexmap::IndexMap; use itertools::Itertools; use pep440_rs::VersionSpecifiers; use pep508_rs::{Requirement, VersionOrUrl::VersionSpecifier}; +use pixi_config::PinningStrategy; use pixi_manifest::{ pypi::PyPiPackageName, DependencyOverwriteBehavior, FeatureName, FeaturesExt, HasFeaturesIter, SpecType, @@ -15,16 +25,6 @@ use pixi_manifest::{ use rattler_conda_types::{MatchSpec, PackageName, Platform, Version}; use rattler_lock::{LockFile, Package}; -use super::has_specs::HasSpecs; -use crate::environment::LockFileUsage; -use crate::{ - cli::cli_config::{DependencyConfig, PrefixUpdateConfig, ProjectConfig}, - environment::verify_prefix_location_unchanged, - load_lock_file, - lock_file::{filter_lock_file, LockFileDerivedData, UpdateContext}, - project::{grouped_environment::GroupedEnvironment, DependencyType, Project}, -}; - /// Adds dependencies to the project /// /// The dependencies should be defined as MatchSpec for conda package, or a PyPI @@ -380,18 +380,25 @@ fn update_conda_specs_from_lock_file( .flatten() .collect_vec(); - let pinning_strategy = project.config().pinning_strategy.unwrap_or_default(); + let mut pinning_strategy = project.config().pinning_strategy; let channel_config = project.channel_config(); for (name, (spec_type, spec)) in conda_specs_to_add_constraints_for { - let version_constraint = pinning_strategy.determine_version_constraint( - conda_records.iter().filter_map(|record| { + // Edge case: python is a special case where we want to pin the minor version by default. + // This is done to avoid early user confusion when the minor version changes and environments magically start breaking. + // This move a `>=3.13, <4` to a `>=3.13, <3.14` constraint. + if name.as_normalized() == "python" && pinning_strategy.is_none() { + pinning_strategy = Some(PinningStrategy::Minor); + } + + let version_constraint = pinning_strategy + .unwrap_or_default() + .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 diff --git a/tests/add_tests.rs b/tests/add_tests.rs index e762e021b..a0da9e93b 100644 --- a/tests/add_tests.rs +++ b/tests/add_tests.rs @@ -515,3 +515,66 @@ async fn add_unconstrainted_dependency() { bar = "*" "###); } + +#[tokio::test] +async fn pinning_dependency() { + // Create a channel with a single package + let mut package_database = PackageDatabase::default(); + package_database.add_package(Package::build("foobar", "1").finish()); + package_database.add_package(Package::build("python", "3.13").finish()); + + let local_channel = package_database.into_channel().await.unwrap(); + + // Initialize a new pixi project using the above channel + let pixi = PixiControl::new().unwrap(); + pixi.init().with_channel(local_channel.url()).await.unwrap(); + + // Add the `packages` to the project + pixi.add("foobar").await.unwrap(); + pixi.add("python").await.unwrap(); + + let project = pixi.project().unwrap(); + + // Get the specs for the `python` package + let python_spec = project + .manifest() + .default_feature() + .dependencies(None, None) + .unwrap_or_default() + .get("python") + .cloned() + .unwrap() + .to_toml_value() + .to_string(); + // Testing to see if edge cases are handled correctly + // Python shouldn't be automatically pinned to a major version. + assert_eq!(python_spec, r#"">=3.13,<3.14""#); + + // Get the specs for the `foobar` package + let foobar_spec = project + .manifest() + .default_feature() + .dependencies(None, None) + .unwrap_or_default() + .get("foobar") + .cloned() + .unwrap() + .to_toml_value() + .to_string(); + assert_eq!(foobar_spec, r#"">=1,<2""#); + + // Add the `python` package with a specific version + pixi.add("python==3.13").await.unwrap(); + let project = pixi.project().unwrap(); + let python_spec = project + .manifest() + .default_feature() + .dependencies(None, None) + .unwrap_or_default() + .get("python") + .cloned() + .unwrap() + .to_toml_value() + .to_string(); + assert_eq!(python_spec, r#""==3.13""#); +}