Skip to content

Commit 52fa5c9

Browse files
committed
feat: support pip style no-deps
1 parent b83ed6e commit 52fa5c9

File tree

40 files changed

+1224
-193
lines changed

40 files changed

+1224
-193
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pixi/tests/integration_rust/add_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ index-url = "{index_url}"
543543
spec,
544544
PixiPypiSpec {
545545
extras: vec![pep508_rs::ExtraName::from_str("cli").unwrap()],
546+
no_deps: false,
546547
source: PixiPypiSource::Registry {
547548
version: VersionOrStar::from_str("==24.8.0").unwrap(),
548549
index: None,

crates/pixi/tests/integration_rust/common/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ impl PixiControl {
447447
backend_override: self.backend_override.clone(),
448448
},
449449
dependency_config: AddBuilder::dependency_config_with_specs(specs),
450+
pypi_no_deps_config: Default::default(),
450451
no_install_config: NoInstallConfig { no_install: true },
451452
lock_file_update_config: LockFileUpdateConfig {
452453
no_lockfile_update: false,
@@ -660,6 +661,7 @@ impl PixiControl {
660661
frozen: false,
661662
locked: false,
662663
},
664+
pypi_no_deps_config: Default::default(),
663665
config: Default::default(),
664666
all: false,
665667
skip: None,

crates/pixi/tests/integration_rust/install_filter_tests.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ async fn install_filter_target_with_skip_with_deps_stop() {
177177
async fn install_subset_e2e_skip_with_deps() {
178178
use std::path::{Path, PathBuf};
179179

180+
use temp_env;
180181
use url::Url;
181182

182183
// manifest with dependent packages: dummy-g depends on dummy-b
@@ -204,11 +205,19 @@ async fn install_subset_e2e_skip_with_deps() {
204205

205206
// Hard-skip dummy-g subtree: expect dummy-g absent, and since dummy-g depends
206207
// on dummy-b, dummy-b is also absent
207-
pixi.install()
208-
.with_frozen()
209-
.with_skipped_with_deps(vec!["dummy-g".into()])
210-
.await
211-
.unwrap();
208+
let cache_dir = pixi.workspace_path().join(".pixi-cache");
209+
fs_err::create_dir_all(&cache_dir).expect("create cache dir");
210+
temp_env::async_with_vars(
211+
[("PIXI_CACHE_DIR", Some(cache_dir.to_str().unwrap()))],
212+
async {
213+
pixi.install()
214+
.with_frozen()
215+
.with_skipped_with_deps(vec!["dummy-g".into()])
216+
.await
217+
},
218+
)
219+
.await
220+
.unwrap();
212221
let prefix = pixi.default_env_path().unwrap();
213222
// When filtering is active, the environment file should contain an invalid hash
214223
let env_file = prefix.join("conda-meta").join("pixi");

crates/pixi/tests/integration_rust/pypi_tests.rs

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use typed_path::Utf8TypedPath;
88
use crate::common::pypi_index::{Database as PyPIDatabase, PyPIPackage};
99
use crate::common::{LockFileExt, PixiControl};
1010
use crate::setup_tracing;
11-
use pixi_test_utils::{MockRepoData, Package};
11+
use pixi_consts::consts;
12+
use pixi_test_utils::{GitRepoFixture, MockRepoData, Package};
1213

1314
/// Helper to check if a pypi package is installed as editable by looking for a .pth file.
1415
/// Editable installations create a .pth file in site-packages that points to the source directory.
@@ -1460,3 +1461,109 @@ version = "0.1.0"
14601461
"Package should NOT be installed as editable when manifest doesn't specify editable = true (even if lock file has editable: true)"
14611462
);
14621463
}
1464+
1465+
#[tokio::test]
1466+
async fn test_no_deps_git_skips_transitive_dependencies() {
1467+
setup_tracing();
1468+
1469+
let platform = Platform::current();
1470+
1471+
let mut package_db = MockRepoData::default();
1472+
package_db.add_package(
1473+
Package::build("python", "3.12.0")
1474+
.with_subdir(platform)
1475+
.finish(),
1476+
);
1477+
let channel = package_db.into_channel().await.unwrap();
1478+
1479+
let pypi_index = PyPIDatabase::new()
1480+
.with(PyPIPackage::new("dep", "1.0.0"))
1481+
.into_simple_index()
1482+
.unwrap();
1483+
1484+
let git_fixture = GitRepoFixture::new("pypi-deps-package");
1485+
1486+
let pixi = PixiControl::from_manifest(&format!(
1487+
r#"
1488+
[workspace]
1489+
name = "pypi-no-deps-git"
1490+
platforms = ["{platform}"]
1491+
channels = ["{channel}"]
1492+
conda-pypi-map = {{}}
1493+
1494+
[dependencies]
1495+
python = "==3.12.0"
1496+
1497+
[pypi-dependencies]
1498+
deps-package = {{ git = "{git_url}", no-deps = true }}
1499+
1500+
[pypi-options]
1501+
extra-index-urls = ["{index_url}"]
1502+
"#,
1503+
platform = platform,
1504+
channel = channel.url(),
1505+
git_url = git_fixture.url,
1506+
index_url = pypi_index.index_url(),
1507+
));
1508+
1509+
let lock_file = pixi.unwrap().update_lock_file().await.unwrap();
1510+
1511+
assert!(lock_file.contains_pypi_package(
1512+
consts::DEFAULT_ENVIRONMENT_NAME,
1513+
platform,
1514+
"deps-package"
1515+
));
1516+
assert!(!lock_file.contains_pypi_package(consts::DEFAULT_ENVIRONMENT_NAME, platform, "dep"));
1517+
}
1518+
1519+
#[tokio::test]
1520+
async fn test_no_deps_path_skips_transitive_dependencies() {
1521+
setup_tracing();
1522+
1523+
let platform = Platform::current();
1524+
1525+
let mut package_db = MockRepoData::default();
1526+
package_db.add_package(
1527+
Package::build("python", "3.12.0")
1528+
.with_subdir(platform)
1529+
.finish(),
1530+
);
1531+
let channel = package_db.into_channel().await.unwrap();
1532+
1533+
let index = PyPIDatabase::new()
1534+
.with(PyPIPackage::new("pathpkg", "1.0.0").with_requires_dist(["dep>=1.0.0"]))
1535+
.with(PyPIPackage::new("dep", "1.0.0"))
1536+
.into_flat_index()
1537+
.expect("failed to create local flat index");
1538+
1539+
let wheel_path = index
1540+
.path()
1541+
.join("pathpkg-1.0.0-py3-none-any.whl")
1542+
.display()
1543+
.to_string()
1544+
.replace('\\', "/");
1545+
1546+
let pixi = PixiControl::from_manifest(&format!(
1547+
r#"
1548+
[workspace]
1549+
name = "pypi-no-deps-path"
1550+
platforms = ["{platform}"]
1551+
channels = ["{channel}"]
1552+
conda-pypi-map = {{}}
1553+
1554+
[dependencies]
1555+
python = "==3.12.0"
1556+
1557+
[pypi-dependencies]
1558+
pathpkg = {{ path = "{wheel_path}", no-deps = true }}
1559+
"#,
1560+
platform = platform,
1561+
channel = channel.url(),
1562+
wheel_path = wheel_path,
1563+
));
1564+
1565+
let lock_file = pixi.unwrap().update_lock_file().await.unwrap();
1566+
1567+
assert!(lock_file.contains_pypi_package(consts::DEFAULT_ENVIRONMENT_NAME, platform, "pathpkg"));
1568+
assert!(!lock_file.contains_pypi_package(consts::DEFAULT_ENVIRONMENT_NAME, platform, "dep"));
1569+
}

crates/pixi_api/src/workspace/reinstall/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub async fn reinstall<I: Interface>(
4646
UpdateLockFileOptions {
4747
lock_file_usage,
4848
no_install: false,
49+
pypi_no_deps: false,
4950
max_concurrent_solves: workspace.config().max_concurrent_solves(),
5051
},
5152
options.reinstall_packages.clone(),

crates/pixi_api/src/workspace/remove/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub async fn remove_conda_deps(
5959
UpdateLockFileOptions {
6060
lock_file_usage: options.lock_file_usage,
6161
no_install: options.no_install,
62+
pypi_no_deps: false,
6263
max_concurrent_solves: workspace.config().max_concurrent_solves(),
6364
},
6465
ReinstallPackages::default(),
@@ -96,6 +97,7 @@ pub async fn remove_pypi_deps(
9697
UpdateLockFileOptions {
9798
lock_file_usage: options.lock_file_usage,
9899
no_install: options.no_install,
100+
pypi_no_deps: false,
99101
max_concurrent_solves: workspace.config().max_concurrent_solves(),
100102
},
101103
ReinstallPackages::default(),

crates/pixi_api/src/workspace/workspace/channel.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub async fn add<I: Interface>(
5656
UpdateLockFileOptions {
5757
lock_file_usage: options.lock_file_usage,
5858
no_install: options.no_install,
59+
pypi_no_deps: false,
5960
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
6061
},
6162
ReinstallPackages::default(),
@@ -97,6 +98,7 @@ pub async fn remove<I: Interface>(
9798
UpdateLockFileOptions {
9899
lock_file_usage: options.lock_file_usage,
99100
no_install: options.no_install,
101+
pypi_no_deps: false,
100102
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
101103
},
102104
ReinstallPackages::default(),
@@ -136,6 +138,7 @@ pub async fn set<I: Interface>(
136138
UpdateLockFileOptions {
137139
lock_file_usage: options.lock_file_usage,
138140
no_install: options.no_install,
141+
pypi_no_deps: false,
139142
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
140143
},
141144
ReinstallPackages::default(),

crates/pixi_api/src/workspace/workspace/platform.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub async fn add<I: Interface>(
4343
UpdateLockFileOptions {
4444
lock_file_usage: LockFileUsage::Update,
4545
no_install,
46+
pypi_no_deps: false,
4647
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
4748
},
4849
ReinstallPackages::default(),
@@ -87,6 +88,7 @@ pub async fn remove<I: Interface>(
8788
UpdateLockFileOptions {
8889
lock_file_usage: LockFileUsage::Update,
8990
no_install,
91+
pypi_no_deps: false,
9092
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
9193
},
9294
ReinstallPackages::default(),

crates/pixi_cli/src/add.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use clap::Parser;
2+
use miette::IntoDiagnostic;
23
use pixi_api::{
34
WorkspaceContext,
45
workspace::{DependencyOptions, GitOptions},
56
};
67
use pixi_config::ConfigCli;
78
use pixi_core::{DependencyType, WorkspaceLocator};
9+
use pixi_pypi_spec::PixiPypiSpec;
810

911
use crate::{
1012
cli_config::{DependencyConfig, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig},
@@ -80,6 +82,9 @@ pub struct Args {
8082
#[clap(flatten)]
8183
pub dependency_config: DependencyConfig,
8284

85+
#[clap(flatten)]
86+
pub pypi_no_deps_config: crate::cli_config::PypiNoDepsConfig,
87+
8388
#[clap(flatten)]
8489
pub no_install_config: NoInstallConfig,
8590

@@ -158,21 +163,44 @@ pub async fn execute(args: Args) -> miette::Result<()> {
158163
.await?
159164
}
160165
DependencyType::PypiDependency => {
166+
let no_deps = args.pypi_no_deps_config.no_deps;
161167
let pypi_deps = match args
162168
.dependency_config
163169
.vcs_pep508_requirements(&workspace)
164170
.transpose()?
165171
{
166172
Some(vcs_reqs) => vcs_reqs
167173
.into_iter()
168-
.map(|(name, req)| (name, (req, None, None)))
169-
.collect(),
174+
.map(|(name, req)| {
175+
let pixi_req = if no_deps {
176+
Some(
177+
PixiPypiSpec::try_from(req.clone())
178+
.into_diagnostic()?
179+
.with_no_deps(true),
180+
)
181+
} else {
182+
None
183+
};
184+
Ok((name, (req, pixi_req, None)))
185+
})
186+
.collect::<miette::Result<_>>()?,
170187
None => args
171188
.dependency_config
172189
.pypi_deps(&workspace)?
173190
.into_iter()
174-
.map(|(name, req)| (name, (req, None, None)))
175-
.collect(),
191+
.map(|(name, req)| {
192+
let pixi_req = if no_deps {
193+
Some(
194+
PixiPypiSpec::try_from(req.clone())
195+
.into_diagnostic()?
196+
.with_no_deps(true),
197+
)
198+
} else {
199+
None
200+
};
201+
Ok((name, (req, pixi_req, None)))
202+
})
203+
.collect::<miette::Result<_>>()?,
176204
};
177205

178206
workspace_ctx

0 commit comments

Comments
 (0)