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
60 changes: 60 additions & 0 deletions crates/pixi/tests/integration_rust/add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
5 changes: 5 additions & 0 deletions crates/pixi/tests/integration_rust/common/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/pixi/tests/integration_rust/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ impl PixiControl {
},
config: Default::default(),
editable: false,
index: None,
},
}
}
Expand Down
26 changes: 24 additions & 2 deletions crates/pixi_cli/src/add.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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<String>,
}

impl TryFrom<&Args> for DependencyOptions {
Expand Down Expand Up @@ -158,20 +165,35 @@ 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)
.transpose()?
{
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(),
};

Expand Down
2 changes: 2 additions & 0 deletions docs/reference/cli/pixi/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pixi add [OPTIONS] <SPEC>...
<br>**default**: `default`
- <a id="arg---editable" href="#arg---editable">`--editable`</a>
: Whether the pypi requirement should be editable
- <a id="arg---index" href="#arg---index">`--index <INDEX>`</a>
: The URL of the PyPI index to use for this dependency

## Config Options
- <a id="arg---auth-file" href="#arg---auth-file">`--auth-file <AUTH_FILE>`</a>
Expand Down
Loading