diff --git a/crates/pixi/tests/integration_rust/add_tests.rs b/crates/pixi/tests/integration_rust/add_tests.rs index 64599a9b8f..bacd87af26 100644 --- a/crates/pixi/tests/integration_rust/add_tests.rs +++ b/crates/pixi/tests/integration_rust/add_tests.rs @@ -1205,3 +1205,63 @@ preview = ['pixi-build'] insta::assert_snapshot!(workspace.workspace.provenance.read().unwrap().into_inner()); }); } +/// Test the `pixi add --pypi --index` functionality +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn add_pypi_with_index() { + use crate::common::pypi_index::{Database as PyPIDatabase, PyPIPackage}; + setup_tracing(); + // Create local PyPI index with test package + let pypi_index = PyPIDatabase::new() + .with(PyPIPackage::new("requests", "2.32.0")) + .into_simple_index() + .unwrap(); + // Create local conda channel with Python + let mut package_db = MockRepoData::default(); + package_db.add_package( + Package::build("python", "3.12.0") + .with_subdir(Platform::current()) + .finish(), + ); + let channel = package_db.into_channel().await.unwrap(); + let pixi = PixiControl::new().unwrap(); + pixi.init() + .without_channels() + .with_local_channel(channel.url().to_file_path().unwrap()) + .await + .unwrap(); + // Add python + pixi.add("python==3.12.0") + .set_type(DependencyType::CondaDependency(SpecType::Run)) + .await + .unwrap(); + // Add a pypi package with custom index + let custom_index = pypi_index.index_url().to_string(); + pixi.add("requests") + .set_type(DependencyType::PypiDependency) + .with_index(custom_index.clone()) + .await + .unwrap(); + // Read project and check if index is set + let project = Workspace::from_path(pixi.manifest_path().as_path()).unwrap(); + let pypi_deps: Vec<_> = project + .default_environment() + .pypi_dependencies(None) + .into_specs() + .collect(); + // Find the requests package + let (_name, spec) = pypi_deps + .iter() + .find(|(name, _)| *name == PypiPackageName::from_str("requests").unwrap()) + .expect("requests package should be in dependencies"); + // Verify the index is set correctly + if let PixiPypiSpec::Version { index, .. } = spec { + assert_eq!( + index.as_ref().map(|u| u.as_str()), + Some(custom_index.as_str()), + "Index URL should match the provided custom index" + ); + } else { + panic!("Expected PixiPypiSpec::Version variant"); + } +} diff --git a/crates/pixi/tests/integration_rust/common/builders.rs b/crates/pixi/tests/integration_rust/common/builders.rs index ad737970bf..d9aa31722d 100644 --- a/crates/pixi/tests/integration_rust/common/builders.rs +++ b/crates/pixi/tests/integration_rust/common/builders.rs @@ -244,6 +244,11 @@ impl AddBuilder { self } + pub fn with_index(mut self, index: String) -> Self { + self.args.index = Some(index); + self + } + /// Deprecated: Use .with_frozen(true).with_install(false) instead pub fn with_no_lockfile_update(mut self, no_lockfile_update: bool) -> Self { if no_lockfile_update { diff --git a/crates/pixi/tests/integration_rust/common/mod.rs b/crates/pixi/tests/integration_rust/common/mod.rs index 9bfd856367..2289436845 100644 --- a/crates/pixi/tests/integration_rust/common/mod.rs +++ b/crates/pixi/tests/integration_rust/common/mod.rs @@ -454,6 +454,7 @@ impl PixiControl { }, config: Default::default(), editable: false, + index: None, }, } } diff --git a/crates/pixi_cli/src/add.rs b/crates/pixi_cli/src/add.rs index e3daa084f4..48b8da976d 100644 --- a/crates/pixi_cli/src/add.rs +++ b/crates/pixi_cli/src/add.rs @@ -1,10 +1,13 @@ use clap::Parser; +use miette::{IntoDiagnostic, WrapErr}; use pixi_api::{ WorkspaceContext, workspace::{DependencyOptions, GitOptions}, }; use pixi_config::ConfigCli; use pixi_core::{DependencyType, WorkspaceLocator}; +use pixi_pypi_spec::PixiPypiSpec; +use url::Url; use crate::{ cli_config::{DependencyConfig, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}, @@ -92,6 +95,10 @@ pub struct Args { /// Whether the pypi requirement should be editable #[arg(long, requires = "pypi")] pub editable: bool, + + /// The URL of the PyPI index to use for this dependency + #[arg(long, requires = "pypi")] + pub index: Option, } impl TryFrom<&Args> for DependencyOptions { @@ -158,6 +165,21 @@ pub async fn execute(args: Args) -> miette::Result<()> { .await? } DependencyType::PypiDependency => { + // Parse the index URL if provided + let index_url = args + .index + .as_ref() + .map(|url_str| Url::parse(url_str)) + .transpose() + .into_diagnostic() + .wrap_err("Failed to parse index URL")?; + + // Create PixiPypiSpec if index is provided + let pixi_spec = index_url.map(|url| PixiPypiSpec::Version { + version: pixi_pypi_spec::VersionOrStar::Star, + extras: Vec::new(), + index: Some(url), + }); let pypi_deps = match args .dependency_config .vcs_pep508_requirements(&workspace) @@ -165,13 +187,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { { Some(vcs_reqs) => vcs_reqs .into_iter() - .map(|(name, req)| (name, (req, None, None))) + .map(|(name, req)| (name, (req, pixi_spec.clone(), None))) .collect(), None => args .dependency_config .pypi_deps(&workspace)? .into_iter() - .map(|(name, req)| (name, (req, None, None))) + .map(|(name, req)| (name, (req, pixi_spec.clone(), None))) .collect(), }; diff --git a/docs/reference/cli/pixi/add.md b/docs/reference/cli/pixi/add.md index 3ad53ee7a2..837f6b5f82 100644 --- a/docs/reference/cli/pixi/add.md +++ b/docs/reference/cli/pixi/add.md @@ -30,6 +30,8 @@ pixi add [OPTIONS] ...
**default**: `default` - `--editable` : Whether the pypi requirement should be editable +- `--index ` +: The URL of the PyPI index to use for this dependency ## Config Options - `--auth-file `