Skip to content
Open
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
41 changes: 41 additions & 0 deletions crates/pixi/tests/integration_rust/add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,47 @@ async fn pinning_dependency() {
assert_eq!(python_spec, r#""==3.13""#);
}

#[tokio::test]
async fn add_existing_dependency_without_version_is_noop() {
setup_tracing();

let mut package_database = MockRepoData::default();
package_database.add_package(Package::build("foobar", "1").finish());
package_database.add_package(Package::build("foobar", "2").finish());
let local_channel = package_database.into_channel().await.unwrap();

let pixi = PixiControl::new().unwrap();
pixi.init().with_channel(local_channel.url()).await.unwrap();

// Add with an explicit version
pixi.add("foobar==1").await.unwrap();

let get_spec = |pixi: &PixiControl| -> String {
pixi.workspace()
.unwrap()
.workspace
.value
.default_feature()
.dependencies(SpecType::Run, None)
.unwrap_or_default()
.get_single("foobar")
.unwrap()
.unwrap()
.clone()
.to_toml_value()
.to_string()
};
assert_eq!(get_spec(&pixi), r#""==1""#);

// Re-add without a version — should be a noop, spec should remain ==1
pixi.add("foobar").await.unwrap();
assert_eq!(get_spec(&pixi), r#""==1""#);

// Re-add with an explicit version — should overwrite
pixi.add("foobar==2").await.unwrap();
assert_eq!(get_spec(&pixi), r#""==2""#);
}

#[tokio::test]
async fn add_dependency_pinning_strategy() {
setup_tracing();
Expand Down
8 changes: 5 additions & 3 deletions crates/pixi/tests/integration_rust/install_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,19 +635,21 @@ async fn minimal_lockfile_update_pypi() {
pep508_rs::Requirement::from_str("click==7.1.2").unwrap()
));

// Widening the click version to allow for the latest version
// Re-adding click without a version is a noop since it's already present.
// Only uvicorn gets updated.
pixi.add_multiple(vec!["uvicorn==0.29.0", "click"])
.set_type(pixi_core::DependencyType::PypiDependency)
.with_install(true)
.await
.unwrap();

// `click` should not be updated to a higher version.
Comment thread
baszalmstra marked this conversation as resolved.
// `click` should remain at its original pinned version since the re-add
// without a version was skipped.
let lock = pixi.lock_file().await.unwrap();
assert!(lock.contains_pep508_requirement(
consts::DEFAULT_ENVIRONMENT_NAME,
Platform::current(),
pep508_rs::Requirement::from_str("click>7.1.2").unwrap()
pep508_rs::Requirement::from_str("click==7.1.2").unwrap()
));
}

Expand Down
2 changes: 2 additions & 0 deletions crates/pixi/tests/integration_rust/upgrade_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use indexmap::IndexMap;
use insta::assert_snapshot;
use pixi_cli::upgrade::{Args, parse_specs_for_platform};
use pixi_core::Workspace;
use pixi_manifest::DependencyOverwriteBehavior;
use rattler_conda_types::Platform;
use tempfile::TempDir;
use url::Url;
Expand Down Expand Up @@ -77,6 +78,7 @@ async fn pypi_dependency_index_preserved_on_upgrade() {
&[],
true,
args.dry_run,
DependencyOverwriteBehavior::Overwrite,
)
.await
.unwrap();
Expand Down
4 changes: 2 additions & 2 deletions crates/pixi_api/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ impl<I: Interface> WorkspaceContext<I> {
spec_type: SpecType,
dep_options: DependencyOptions,
git_options: GitOptions,
) -> miette::Result<Option<UpdateDeps>> {
) -> miette::Result<(Option<UpdateDeps>, Vec<String>)> {
Box::pin(crate::workspace::add::add_conda_dep(
self.workspace_mut()?,
specs,
Expand All @@ -304,7 +304,7 @@ impl<I: Interface> WorkspaceContext<I> {
pypi_deps: PypiDeps,
editable: bool,
options: DependencyOptions,
) -> miette::Result<Option<UpdateDeps>> {
) -> miette::Result<(Option<UpdateDeps>, Vec<String>)> {
Box::pin(crate::workspace::add::add_pypi_dep(
self.workspace_mut()?,
pypi_deps,
Expand Down
24 changes: 13 additions & 11 deletions crates/pixi_api/src/workspace/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use pixi_core::{
environment::sanity_check_workspace,
workspace::{PypiDeps, UpdateDeps, WorkspaceMut},
};
use pixi_manifest::{FeatureName, KnownPreviewFeature, SpecType};
use pixi_manifest::{DependencyOverwriteBehavior, FeatureName, KnownPreviewFeature, SpecType};
use pixi_spec::{GitSpec, SourceLocationSpec, Subdirectory};
use rattler_conda_types::{MatchSpec, PackageName};

Expand All @@ -18,7 +18,7 @@ pub async fn add_conda_dep(
spec_type: SpecType,
dep_options: DependencyOptions,
git_options: GitOptions,
) -> miette::Result<Option<UpdateDeps>> {
) -> miette::Result<(Option<UpdateDeps>, Vec<String>)> {
sanity_check_workspace(workspace.workspace()).await?;

// Add the platform if it is not already present
Expand Down Expand Up @@ -80,7 +80,7 @@ pub async fn add_conda_dep(
// TODO: add dry_run logic to add
let dry_run = false;

let update_deps = match Box::pin(workspace.update_dependencies(
let (update_deps, skipped) = match Box::pin(workspace.update_dependencies(
match_specs,
IndexMap::default(),
source_specs,
Expand All @@ -90,29 +90,30 @@ pub async fn add_conda_dep(
&dep_options.platforms,
false,
dry_run,
DependencyOverwriteBehavior::OverwriteIfExplicit,
))
.await
{
Ok(update_deps) => {
Ok(result) => {
// Write the updated manifest
workspace.save().await.into_diagnostic()?;
update_deps
result
}
Err(e) => {
workspace.revert().await.into_diagnostic()?;
return Err(e);
}
};

Ok(update_deps)
Ok((update_deps, skipped))
}

pub async fn add_pypi_dep(
mut workspace: WorkspaceMut,
pypi_deps: PypiDeps,
editable: bool,
options: DependencyOptions,
) -> miette::Result<Option<UpdateDeps>> {
) -> miette::Result<(Option<UpdateDeps>, Vec<String>)> {
sanity_check_workspace(workspace.workspace()).await?;

// Add the platform if it is not already present
Expand All @@ -123,7 +124,7 @@ pub async fn add_pypi_dep(
// TODO: add dry_run logic to add
let dry_run = false;

let update_deps = match Box::pin(workspace.update_dependencies(
let (update_deps, skipped) = match Box::pin(workspace.update_dependencies(
IndexMap::default(),
pypi_deps,
IndexMap::default(),
Expand All @@ -133,19 +134,20 @@ pub async fn add_pypi_dep(
&options.platforms,
editable,
dry_run,
DependencyOverwriteBehavior::OverwriteIfExplicit,
))
.await
{
Ok(update_deps) => {
Ok(result) => {
// Write the updated manifest
workspace.save().await.into_diagnostic()?;
update_deps
result
}
Err(e) => {
workspace.revert().await.into_diagnostic()?;
return Err(e);
}
};

Ok(update_deps)
Ok((update_deps, skipped))
}
129 changes: 82 additions & 47 deletions crates/pixi_cli/src/add.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use clap::Parser;
use pixi_api::{
WorkspaceContext,
Expand Down Expand Up @@ -136,56 +138,89 @@ pub async fn execute(args: Args) -> miette::Result<()> {

let workspace_ctx = WorkspaceContext::new(CliInterface {}, workspace.clone());

let update_deps = match args.dependency_config.dependency_type() {
DependencyType::CondaDependency(spec_type) => {
let git_options = GitOptions {
git: args.dependency_config.git.clone(),
reference: args
.dependency_config
.rev
.clone()
.unwrap_or_default()
.into(),
subdir: args.dependency_config.subdir.clone(),
};

workspace_ctx
.add_conda_deps(
args.dependency_config.specs()?,
spec_type,
(&args).try_into()?,
git_options,
)
.await?
}
DependencyType::PypiDependency => {
let pypi_deps = match args
.dependency_config
.vcs_pep508_requirements(&workspace)
.transpose()?
{
Some(vcs_reqs) => vcs_reqs
.into_iter()
.map(|(name, req)| (name, (req, None, None)))
.collect(),
None => args
let (update_deps, skipped, parsed_names): (_, Vec<String>, Vec<String>) =
match args.dependency_config.dependency_type() {
DependencyType::CondaDependency(spec_type) => {
let git_options = GitOptions {
git: args.dependency_config.git.clone(),
reference: args
.dependency_config
.rev
.clone()
.unwrap_or_default()
.into(),
subdir: args.dependency_config.subdir.clone(),
};

let specs = args.dependency_config.specs()?;
let names: Vec<String> = specs
.keys()
.map(|n| n.as_normalized().to_string())
.collect();
let result = workspace_ctx
.add_conda_deps(specs, spec_type, (&args).try_into()?, git_options)
.await?;
(result.0, result.1, names)
}
DependencyType::PypiDependency => {
let pypi_deps: pixi_core::workspace::PypiDeps = match args
.dependency_config
.pypi_deps(&workspace)?
.into_iter()
.map(|(name, req)| (name, (req, None, None)))
.collect(),
};

workspace_ctx
.add_pypi_deps(pypi_deps, args.editable, (&args).try_into()?)
.await?
}
};
.vcs_pep508_requirements(&workspace)
.transpose()?
{
Some(vcs_reqs) => vcs_reqs
.into_iter()
.map(|(name, req)| (name, (req, None, None)))
.collect(),
None => args
.dependency_config
.pypi_deps(&workspace)?
.into_iter()
.map(|(name, req)| (name, (req, None, None)))
.collect(),
};

let names: Vec<String> = pypi_deps
.keys()
.map(|n| n.as_normalized().to_string())
.collect();
let result = workspace_ctx
.add_pypi_deps(pypi_deps, args.editable, (&args).try_into()?)
.await?;
(result.0, result.1, names)
}
};

let skipped_set: HashSet<&str> = skipped.iter().map(|s| s.as_str()).collect();

for package in &skipped {
eprintln!(
"{}{} is already a dependency",
console::style(console::Emoji("✔ ", "")).green(),
console::style(package).bold(),
);
eprintln!(
" Run `{}` to get the newest compatible version",
console::style(format!("pixi upgrade {package}"))
.green()
.bold(),
);
}

if let Some(update_deps) = update_deps {
// Notify the user we succeeded
args.dependency_config
.display_success("Added", update_deps.implicit_constraints);
let added_specs: Vec<String> = args
.dependency_config
.specs
.iter()
.zip(parsed_names.iter())
.filter(|(_, name)| !skipped_set.contains(name.as_str()))
.map(|(raw, _)| raw.clone())
.collect();
let display_config = DependencyConfig {
specs: added_specs,
..args.dependency_config
};
display_config.display_success("Added", update_deps.implicit_constraints);
}

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions crates/pixi_cli/src/cli_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,12 @@ impl DependencyConfig {
operation: &str,
implicit_constraints: HashMap<String, String>,
) {
for package in self.specs.clone() {
for package in &self.specs {
eprintln!(
"{}{operation} {}{}",
console::style(console::Emoji("✔ ", "")).green(),
console::style(&package).bold(),
if let Some(constraint) = implicit_constraints.get(&package) {
if let Some(constraint) = implicit_constraints.get(package.as_str()) {
format!(" {}", console::style(constraint).dim())
} else {
"".to_string()
Expand Down
Loading
Loading