diff --git a/Cargo.lock b/Cargo.lock index 29b1e8ab4..cbe520585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,6 +844,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "concat-idents" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" +dependencies = [ + "quote", + "syn 2.0.66", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1351,12 +1361,13 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "file_url" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd0e1a8a0c7a1829090615ebce07ebf31911031a08915ca869f1085f0e39032" +checksum = "1042c5fdc9f2cf548a139ccd0985fa2460d796f99b08574f72f1f53d179e6591" dependencies = [ "itertools 0.12.1", "percent-encoding", + "thiserror", "typed-path", "url", ] @@ -3275,6 +3286,7 @@ dependencies = [ "clap", "clap-verbosity-flag", "clap_complete", + "concat-idents", "console", "crossbeam-channel", "csv", @@ -3640,9 +3652,9 @@ dependencies = [ [[package]] name = "rattler" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4dbdaed42ff38c8bcccef910b960d4db52fcc3f774f485bb18de691b9e8fe94" +checksum = "f3d5504e8afc260cceebb79886032ac146c9344c55fbaf9034ca45a0c00b7447" dependencies = [ "anyhow", "bytes", @@ -3662,6 +3674,7 @@ dependencies = [ "memmap2 0.9.4", "once_cell", "parking_lot 0.12.3", + "rattler_cache", "rattler_conda_types", "rattler_digest", "rattler_networking", @@ -3682,11 +3695,36 @@ dependencies = [ "uuid", ] +[[package]] +name = "rattler_cache" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad5b1a62c97fe6acbad6f1421eed77bf75f736a5af44a18f0e46d6d1cd5c81" +dependencies = [ + "anyhow", + "chrono", + "digest", + "dirs", + "fxhash", + "itertools 0.12.1", + "parking_lot 0.12.3", + "rattler_conda_types", + "rattler_digest", + "rattler_networking", + "rattler_package_streaming", + "reqwest 0.12.4", + "reqwest-middleware", + "thiserror", + "tokio", + "tracing", + "url", +] + [[package]] name = "rattler_conda_types" -version = "0.25.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2046a77fc9d79879f8cdd798603e6b036ed9ac5a297b5ff25e44ee23fe2fc9" +checksum = "65d6d35c484af9b1a3ce13ace90de388c8a21b1f832bf2ee97b5681a94178326" dependencies = [ "chrono", "file_url", @@ -3731,9 +3769,9 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701ca75a920e2c142538faaed11bf3e983102d149dd50488ace153812c1e0f72" +checksum = "5bb54f27b97a03b9b2921bd18967947bc5788a8d653fcd6b6bdd18dad192312f" dependencies = [ "chrono", "file_url", @@ -3797,9 +3835,9 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca9d94fb324ac9855ac5fdbbc3b53e4ca101e84a270d6a30f059e4319c45b62" +checksum = "390c453b80d7904362e121c89f29aee796fb4d38cc7b24fe7ba9e583ef77a27b" dependencies = [ "bzip2", "chrono", @@ -3875,9 +3913,9 @@ dependencies = [ [[package]] name = "rattler_shell" -version = "0.20.6" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5710a54191836eab4063543ff41b042d12d8a79092fdb568f210b98bd39ce03" +checksum = "e17c8a64079dc3a7b8b0070e0d7c4bee4008bf16d799b8b621a9aa88968650c6" dependencies = [ "enum_dispatch", "indexmap 2.2.6", @@ -3893,9 +3931,9 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b58a172466a9e7338e4fb05ebfa8b9b297de53a5735190880f9d8f17d409c2" +checksum = "d62f673fe9f9198b4d3235da314d727eff59515cc4db9b0e2452e9bbe959433d" dependencies = [ "chrono", "futures", @@ -4218,10 +4256,11 @@ dependencies = [ [[package]] name = "resolvo" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d299d168910c5d71f3c0f5441abe38ca4a6ae21f70fae909bfc6bead28f6620f" +checksum = "e7b73dc355efbb88c372550b92bf17d36bf555ecf319a4783a5b8b7c34488bc5" dependencies = [ + "ahash 0.8.11", "bitvec", "elsa", "event-listener 5.3.0", diff --git a/Cargo.toml b/Cargo.toml index 9b7c708d4..3e2c69964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ clap = { version = "4.5.4", default-features = false, features = [ ] } clap-verbosity-flag = "2.2.0" clap_complete = "4.5.2" +concat-idents = "1.1.5" console = { version = "0.15.8", features = ["windows-console-colors"] } crossbeam-channel = "0.5.12" csv = "1.3.0" @@ -74,18 +75,19 @@ miette = { version = "7.2.0", features = [ minijinja = { version = "1.0.20", features = ["builtins"] } once_cell = "1.19.0" parking_lot = "0.12.2" + pep440_rs = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea04c5cfef685516ab660cf5" } pep508_rs = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea04c5cfef685516ab660cf5" } platform-tags = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea04c5cfef685516ab660cf5" } pypi-types = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea04c5cfef685516ab660cf5" } pyproject-toml = "0.11.0" -rattler = { version = "0.26.1", default-features = false, features = [ +rattler = { version = "0.26.4", default-features = false, features = [ "cli-tools", "indicatif", ] } -rattler_conda_types = { version = "0.25.0", default-features = false } +rattler_conda_types = { version = "0.25.2", default-features = false } rattler_digest = { version = "0.19.4", default-features = false } -rattler_lock = { version = "0.22.9", default-features = false } +rattler_lock = { version = "0.22.12", default-features = false } rattler_networking = { version = "0.20.8", default-features = false } rattler_repodata_gateway = { version = "0.20.3", default-features = false, features = [ "sparse", @@ -94,7 +96,7 @@ rattler_repodata_gateway = { version = "0.20.3", default-features = false, featu rattler_shell = { version = "0.20.6", default-features = false, features = [ "sysinfo", ] } -rattler_solve = { version = "0.23.2", default-features = false, features = [ +rattler_solve = { version = "0.24.2", default-features = false, features = [ "resolvo", ] } @@ -194,16 +196,16 @@ pep508_rs = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea #rattler_shell = { git = "https://github.com/mamba-org/rattler", branch = "main" } #rattler_solve = { git = "https://github.com/mamba-org/rattler", branch = "main" } #rattler_virtual_packages = { git = "https://github.com/mamba-org/rattler", branch = "main" } -# rattler_conda_types = { path = "../rattler-1/crates/rattler_conda_types" } -# rattler_digest = { path = "../rattler-1/crates/rattler_digest" } -# rattler_networking = { path = "../rattler-1/crates/rattler_networking" } -# rattler_repodata_gateway = { path = "../rattler-1/crates/rattler_repodata_gateway" } -# rattler_shell = { path = "../rattler-1/crates/rattler_shell" } -# rattler_solve = { path = "../rattler-1/crates/rattler_solve" } -# rattler_virtual_packages = { path = "../rattler-1/crates/rattler_virtual_packages" } -# rattler_lock = { path = "../rattler-1/crates/rattler_lock" } -# rattler_package_streaming = { path = "../rattler-1/crates/rattler_package_streaming" } -# rattler = { path = "../rattler-1/crates/rattler" } +#rattler_conda_types = { path = "../rattler/crates/rattler_conda_types" } +#rattler_digest = { path = "../rattler/crates/rattler_digest" } +#rattler_networking = { path = "../rattler/crates/rattler_networking" } +#rattler_repodata_gateway = { path = "../rattler/crates/rattler_repodata_gateway" } +#rattler_shell = { path = "../rattler/crates/rattler_shell" } +#rattler_solve = { path = "../rattler/crates/rattler_solve" } +#rattler_virtual_packages = { path = "../rattler/crates/rattler_virtual_packages" } +#rattler_lock = { path = "../rattler/crates/rattler_lock" } +#rattler_package_streaming = { path = "../rattler/crates/rattler_package_streaming" } +#rattler = { path = "../rattler/crates/rattler" } # Change these lines if you want a patched version of uv # [patch.'https://github.com/astral-sh/uv'] # pep440_rs = { git = "https://github.com/astral-sh/uv", rev = "65b17f6e81125064ea04c5cfef685516ab660cf5" } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 041d3eb59..d86ca0928 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -147,6 +147,7 @@ It will only update the lock file if the dependencies in the [manifest file](pro - `--platform (-p)`: The platform for which the dependencies should be updated. - `--dry-run (-n)`: Only show the changes that would be made, without actually updating the lock file or environment. - `--no-install`: Don't install the (solve) environment needed for solving pypi-dependencies. +- `--json`: Output the changes in json format. ```shell pixi update numpy diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs index 27667c385..1ba4e06fa 100644 --- a/src/cli/shell_hook.rs +++ b/src/cli/shell_hook.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, default::Default, path::PathBuf}; + use clap::Parser; use miette::IntoDiagnostic; use rattler_shell::{ @@ -6,13 +8,11 @@ use rattler_shell::{ }; use serde::Serialize; use serde_json; -use std::collections::HashMap; -use std::{default::Default, path::PathBuf}; -use crate::config::ConfigCliPrompt; use crate::{ activation::get_activator, cli::LockFileUsageArgs, + config::ConfigCliPrompt, environment::get_up_to_date_prefix, project::{has_features::HasFeatures, Environment}, Project, @@ -20,10 +20,12 @@ use crate::{ /// Print the pixi environment activation script. /// -/// You can source the script to activate the environment without needing pixi itself. +/// You can source the script to activate the environment without needing pixi +/// itself. #[derive(Parser, Debug)] pub struct Args { - /// Sets the shell, options: [`bash`, `zsh`, `xonsh`, `cmd`, `powershell`, `fish`, `nushell`] + /// Sets the shell, options: [`bash`, `zsh`, `xonsh`, `cmd`, + /// `powershell`, `fish`, `nushell`] #[arg(short, long)] shell: Option, @@ -56,7 +58,8 @@ async fn generate_activation_script( shell: Option, environment: &Environment<'_>, ) -> miette::Result { - // Get shell from the arguments or from the current process or use default if all fails + // Get shell from the arguments or from the current process or use default if + // all fails let shell = shell.unwrap_or_else(|| { ShellEnum::from_parent_process() .unwrap_or_else(|| ShellEnum::from_env().unwrap_or_default()) @@ -68,7 +71,8 @@ async fn generate_activation_script( .ok() .map(|p| std::env::split_paths(&p).collect::>()); - // If we are in a conda environment, we need to deactivate it before activating the host / build prefix + // If we are in a conda environment, we need to deactivate it before activating + // the host / build prefix let conda_prefix = std::env::var("CONDA_PREFIX").ok().map(|p| p.into()); let result = activator .activation(ActivationVariables { @@ -81,8 +85,8 @@ async fn generate_activation_script( result.script.contents().into_diagnostic() } -/// Generates a JSON object describing the changes to the shell environment when activating -/// the provided pixi environment. +/// Generates a JSON object describing the changes to the shell environment when +/// activating the provided pixi environment. async fn generate_environment_json(environment: &Environment<'_>) -> miette::Result { let environment_variables = environment.project().get_env_variables(environment).await?; let shell_env = ShellEnv { @@ -112,17 +116,21 @@ pub async fn execute(args: Args) -> miette::Result<()> { #[cfg(test)] mod tests { + use rattler_conda_types::Platform; + use rattler_shell::shell::{Bash, CmdExe, Fish, NuShell, PowerShell, Shell, Xonsh, Zsh}; + use super::*; - use rattler_shell::shell::{Bash, CmdExe, Fish, NuShell, PowerShell, Xonsh, Zsh}; #[tokio::test] async fn test_shell_hook() { + let default_shell = rattler_shell::shell::ShellEnum::default(); + let path_var_name = default_shell.path_var(&Platform::current()); let project = Project::discover().unwrap(); let environment = project.default_environment(); let script = generate_activation_script(Some(ShellEnum::Bash(Bash)), &environment) .await .unwrap(); - assert!(script.contains("export PATH=")); + assert!(script.contains(&format!("export {path_var_name}="))); assert!(script.contains("export CONDA_PREFIX=")); let script = generate_activation_script( @@ -131,50 +139,49 @@ mod tests { ) .await .unwrap(); - assert!(script.contains("${Env:PATH}")); + assert!(script.contains(&format!("${{Env:{path_var_name}}}"))); assert!(script.contains("${Env:CONDA_PREFIX}")); let script = generate_activation_script(Some(ShellEnum::Zsh(Zsh)), &environment) .await .unwrap(); - assert!(script.contains("export PATH=")); + assert!(script.contains(&format!("export {path_var_name}="))); assert!(script.contains("export CONDA_PREFIX=")); let script = generate_activation_script(Some(ShellEnum::Fish(Fish)), &environment) .await .unwrap(); - assert!(script.contains("set -gx PATH ")); + assert!(script.contains(&format!("set -gx {path_var_name} "))); assert!(script.contains("set -gx CONDA_PREFIX ")); let script = generate_activation_script(Some(ShellEnum::Xonsh(Xonsh)), &environment) .await .unwrap(); - assert!(script.contains("$PATH = ")); + assert!(script.contains(&format!("${path_var_name} = "))); assert!(script.contains("$CONDA_PREFIX = ")); let script = generate_activation_script(Some(ShellEnum::CmdExe(CmdExe)), &environment) .await .unwrap(); - assert!(script.contains("@SET \"PATH=")); + assert!(script.contains(&format!("@SET \"{path_var_name}="))); assert!(script.contains("@SET \"CONDA_PREFIX=")); let script = generate_activation_script(Some(ShellEnum::NuShell(NuShell)), &environment) .await .unwrap(); - assert!(script.contains("$env.PATH = ")); + assert!(script.contains(&format!("$env.{path_var_name} = "))); assert!(script.contains("$env.CONDA_PREFIX = ")); } #[tokio::test] async fn test_environment_json() { + let default_shell = rattler_shell::shell::ShellEnum::default(); + let path_var_name = default_shell.path_var(&Platform::current()); let project = Project::discover().unwrap(); let environment = project.default_environment(); let json_env = generate_environment_json(&environment).await.unwrap(); assert!(json_env.contains("\"PIXI_ENVIRONMENT_NAME\":\"default\"")); assert!(json_env.contains("\"CONDA_PREFIX\":")); - #[cfg(not(target_os = "windows"))] - assert!(json_env.contains("\"PATH\":")); - #[cfg(target_os = "windows")] - assert!(json_env.contains("\"Path\":")); + assert!(json_env.contains(&format!("\"{path_var_name}\":"))); } } diff --git a/src/cli/update.rs b/src/cli/update.rs index 2d89e16f9..fa9c100af 100644 --- a/src/cli/update.rs +++ b/src/cli/update.rs @@ -1,5 +1,5 @@ -use std::borrow::Cow; use std::{ + borrow::Cow, cmp::Ordering, collections::HashSet, io::{stdout, Write}, @@ -13,13 +13,18 @@ use itertools::{Either, Itertools}; use miette::{Context, IntoDiagnostic, MietteDiagnostic}; use rattler_conda_types::Platform; use rattler_lock::{LockFile, LockFileBuilder, Package}; +use serde::Serialize; +use serde_json::Value; use tabwriter::TabWriter; -use crate::consts::{CondaEmoji, PypiEmoji}; -use crate::project::grouped_environment::GroupedEnvironment; use crate::{ - config::ConfigCli, consts, load_lock_file, lock_file::UpdateContext, EnvironmentName, - HasFeatures, Project, + config::ConfigCli, + consts, + consts::{CondaEmoji, PypiEmoji}, + load_lock_file, + lock_file::UpdateContext, + project::grouped_environment::GroupedEnvironment, + EnvironmentName, HasFeatures, Project, }; /// Update dependencies as recorded in the local lock file @@ -32,7 +37,8 @@ pub struct Args { #[arg(long)] pub manifest_path: Option, - /// Don't install the (solve) environments needed for pypi-dependencies solving. + /// Don't install the (solve) environments needed for pypi-dependencies + /// solving. #[arg(long)] pub no_install: bool, @@ -42,6 +48,9 @@ pub struct Args { #[clap(flatten)] pub specs: UpdateSpecsArgs, + + #[clap(long)] + pub json: bool, } #[derive(Parser, Debug, Default)] @@ -80,7 +89,8 @@ impl From for UpdateSpecs { } impl UpdateSpecs { - /// Returns true if the package should be relaxed according to the user input. + /// Returns true if the package should be relaxed according to the user + /// input. fn should_relax(&self, environment_name: &str, platform: Platform, package: &Package) -> bool { // Check if the platform is in the list of platforms to update. if let Some(platforms) = &self.platforms { @@ -159,7 +169,14 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Determine the diff between the old and new lock-file. let diff = LockFileDiff::from_lock_files(&loaded_lock_file, &updated_lock_file.lock_file); - if diff.is_empty() { + + // Format as json? + if args.json { + let diff = LockFileDiff::from_lock_files(&loaded_lock_file, &updated_lock_file.lock_file); + let json_diff = LockFileJsonDiff::new(&project, diff); + let json = serde_json::to_string_pretty(&json_diff).expect("failed to convert to json"); + println!("{}", json); + } else if diff.is_empty() { println!( "{}Lock-file was already up-to-date", console::style(console::Emoji("✔ ", "")).green() @@ -622,3 +639,148 @@ impl LockFileDiff { .collect() } } + +#[derive(Serialize, Clone)] +pub struct JsonPackageDiff { + name: String, + before: Option, + after: Option, + #[serde(rename = "type")] + ty: JsonPackageType, + #[serde(skip_serializing_if = "std::ops::Not::not")] + explicit: bool, +} + +#[derive(Serialize, Copy, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum JsonPackageType { + Conda, + Pypi, +} + +#[derive(Serialize, Clone)] +pub struct LockFileJsonDiff { + pub version: usize, + pub environment: IndexMap>>, +} + +impl LockFileJsonDiff { + fn new(project: &Project, value: LockFileDiff) -> Self { + let mut environment = IndexMap::new(); + + for (environment_name, environment_diff) in value.environment { + let mut environment_diff_json = IndexMap::new(); + + for (platform, packages_diff) in environment_diff { + let conda_dependencies = project + .environment(environment_name.as_str()) + .map(|env| env.dependencies(None, Some(platform))) + .unwrap_or_default(); + + let pypi_dependencies = project + .environment(environment_name.as_str()) + .map(|env| env.pypi_dependencies(Some(platform))) + .unwrap_or_default(); + + let add_diffs = packages_diff.added.into_iter().map(|new| match new { + Package::Conda(pkg) => JsonPackageDiff { + name: pkg.package_record().name.as_normalized().to_string(), + before: None, + after: Some(serde_json::to_value(&pkg).unwrap()), + ty: JsonPackageType::Conda, + explicit: conda_dependencies.contains_key(&pkg.package_record().name), + }, + Package::Pypi(pkg) => JsonPackageDiff { + name: pkg.data().package.name.as_dist_info_name().into_owned(), + before: None, + after: Some(serde_json::to_value(&pkg).unwrap()), + ty: JsonPackageType::Pypi, + explicit: pypi_dependencies.contains_key(&pkg.data().package.name), + }, + }); + + let removed_diffs = packages_diff.removed.into_iter().map(|old| match old { + Package::Conda(pkg) => JsonPackageDiff { + name: pkg.package_record().name.as_normalized().to_string(), + before: Some(serde_json::to_value(&pkg).unwrap()), + after: None, + ty: JsonPackageType::Conda, + explicit: conda_dependencies.contains_key(&pkg.package_record().name), + }, + + Package::Pypi(pkg) => JsonPackageDiff { + name: pkg.data().package.name.as_dist_info_name().into_owned(), + before: Some(serde_json::to_value(&pkg).unwrap()), + after: None, + ty: JsonPackageType::Pypi, + explicit: pypi_dependencies.contains_key(&pkg.data().package.name), + }, + }); + + let changed_diffs = packages_diff.changed.into_iter().map(|(old, new)| match (old, new) { + (Package::Conda(old), Package::Conda(new)) => + { + let before = serde_json::to_value(&old).unwrap(); + let after = serde_json::to_value(&new).unwrap(); + let (before, after) = compute_json_diff(before, after); + JsonPackageDiff { + name: old.package_record().name.as_normalized().to_string(), + before: Some(before), + after: Some(after), + ty: JsonPackageType::Conda, + explicit: conda_dependencies.contains_key(&old.package_record().name), + } + } + (Package::Pypi(old), Package::Pypi(new)) => { + let before = serde_json::to_value(&old).unwrap(); + let after = serde_json::to_value(&new).unwrap(); + let (before, after) = compute_json_diff(before, after); + JsonPackageDiff { + name: old.data().package.name.as_dist_info_name().into_owned(), + before: Some(before), + after: Some(after), + ty: JsonPackageType::Pypi, + explicit: pypi_dependencies.contains_key(&old.data().package.name), + } + } + _ => unreachable!("packages cannot change type, they are represented as removals and inserts instead"), + }); + + let packages_diff_json = add_diffs + .chain(removed_diffs) + .chain(changed_diffs) + .sorted_by_key(|diff| diff.name.clone()) + .collect_vec(); + + environment_diff_json.insert(platform, packages_diff_json); + } + + environment.insert(environment_name, environment_diff_json); + } + + Self { + version: 1, + environment, + } + } +} + +fn compute_json_diff( + mut a: serde_json::Value, + mut b: serde_json::Value, +) -> (serde_json::Value, serde_json::Value) { + if let (Some(a), Some(b)) = (a.as_object_mut(), b.as_object_mut()) { + a.retain(|key, value| { + if let Some(other_value) = b.get(key) { + if other_value == value { + b.remove(key); + return false; + } + } else { + b.insert(key.to_string(), Value::Null); + } + true + }); + } + (a, b) +} diff --git a/src/project/dependencies.rs b/src/project/dependencies.rs index b446ecdb3..9c11bfeb6 100644 --- a/src/project/dependencies.rs +++ b/src/project/dependencies.rs @@ -118,6 +118,14 @@ impl Dependencies { .into_iter() .flat_map(|(name, specs)| specs.into_iter().map(move |spec| (name.clone(), spec))) } + + /// Returns true if the dependency list contains the given package name. + pub fn contains_key(&self, name: &Q) -> bool + where + Q: Hash + Equivalent, + { + self.map.contains_key(name) + } } impl Dependencies { diff --git a/src/project/manifest/document.rs b/src/project/manifest/document.rs index b3dceb11f..dafc18dc8 100644 --- a/src/project/manifest/document.rs +++ b/src/project/manifest/document.rs @@ -380,6 +380,7 @@ fn nameless_match_spec_to_toml(spec: &NamelessMatchSpec) -> Value { namespace: None, md5: None, sha256: None, + url: None, } => { // No other fields besides the version was specified, so we can just return the // version as a string. @@ -398,6 +399,7 @@ fn nameless_match_spec_to_toml(spec: &NamelessMatchSpec) -> Value { namespace, md5, sha256, + url, } => { let mut table = InlineTable::new(); table.insert( @@ -437,6 +439,9 @@ fn nameless_match_spec_to_toml(spec: &NamelessMatchSpec) -> Value { if let Some(sha256) = sha256 { table.insert("sha256", format!("{:x}", sha256).into()); } + if let Some(url) = url { + table.insert("url", url.to_string().into()); + } table.into() } } diff --git a/src/project/manifest/python.rs b/src/project/manifest/python.rs index 419dbe18e..a400d4396 100644 --- a/src/project/manifest/python.rs +++ b/src/project/manifest/python.rs @@ -1,14 +1,17 @@ +use std::{ + borrow::Borrow, + fmt, + fmt::{Display, Formatter}, + path::{Path, PathBuf}, + str::FromStr, +}; + use pep440_rs::VersionSpecifiers; use pep508_rs::VerbatimUrl; use pypi_types::VerbatimParsedUrl; -use serde::Serializer; -use serde::{de::Error, Deserialize, Deserializer, Serialize}; -use std::fmt::Display; -use std::path::{Path, PathBuf}; -use std::{fmt, fmt::Formatter, str::FromStr}; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use url::Url; - use uv_normalize::{ExtraName, InvalidNameError, PackageName}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -18,6 +21,12 @@ pub struct PyPiPackageName { normalized: PackageName, } +impl Borrow for PyPiPackageName { + fn borrow(&self) -> &PackageName { + &self.normalized + } +} + impl<'de> Deserialize<'de> for PyPiPackageName { fn deserialize(deserializer: D) -> Result where @@ -453,7 +462,8 @@ impl RequirementOrEditable { } } - /// Returns a pep508 requirement if it is a pep508 requirement, using the parsed url type. + /// Returns a pep508 requirement if it is a pep508 requirement, using the + /// parsed url type. pub fn into_requirement_with_parsed_url( self, ) -> Option> { @@ -592,11 +602,13 @@ impl PyPiRequirement { #[cfg(test)] mod tests { - use super::*; + use std::str::FromStr; + use indexmap::IndexMap; use insta::assert_snapshot; use pep508_rs::Requirement; - use std::str::FromStr; + + use super::*; #[test] fn test_pypi_to_string() { diff --git a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.cockpit.yml.snap b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.cockpit.yml.snap index ae83b0e74..70853f092 100644 --- a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.cockpit.yml.snap +++ b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.cockpit.yml.snap @@ -21,6 +21,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -38,6 +39,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -77,6 +79,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -116,6 +119,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -155,6 +159,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -194,6 +199,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -233,6 +239,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -272,6 +279,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -311,6 +319,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -350,6 +359,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -389,6 +399,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -428,6 +439,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -467,6 +479,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -506,6 +519,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -545,6 +559,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -584,6 +599,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -623,6 +639,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -662,6 +679,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -701,6 +719,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -750,6 +769,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -789,6 +809,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, ], [ diff --git a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.conda.yml.snap b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.conda.yml.snap index 06442e412..3eb66f3be 100644 --- a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.conda.yml.snap +++ b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.conda.yml.snap @@ -31,6 +31,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -58,6 +59,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, ], [], diff --git a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.crnnft.yml.snap b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.crnnft.yml.snap index 7840064a4..d4f11fd6a 100644 --- a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.crnnft.yml.snap +++ b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.crnnft.yml.snap @@ -31,6 +31,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -58,6 +59,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -85,6 +87,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -112,6 +115,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, ], [ diff --git a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.let-plot.yml.snap b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.let-plot.yml.snap index 167b8c800..370386c0b 100644 --- a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.let-plot.yml.snap +++ b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.let-plot.yml.snap @@ -31,6 +31,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -56,6 +57,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -83,6 +85,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -108,6 +111,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -133,6 +137,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -158,6 +163,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -183,6 +189,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -200,6 +207,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, ], [ diff --git a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.test_env.yml.snap b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.test_env.yml.snap index f27375587..783e175d4 100644 --- a/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.test_env.yml.snap +++ b/src/utils/snapshots/pixi__utils__conda_environment_file__tests__test_import_from_env_yaml.test_env.yml.snap @@ -31,6 +31,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -56,6 +57,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, MatchSpec { name: Some( @@ -81,6 +83,7 @@ expression: "(parse_dependencies(env_info.dependencies().clone()).unwrap(),\n namespace: None, md5: None, sha256: None, + url: None, }, ], [ diff --git a/tests/common/builders.rs b/tests/common/builders.rs index 510bf3b46..141a81219 100644 --- a/tests/common/builders.rs +++ b/tests/common/builders.rs @@ -397,6 +397,11 @@ impl UpdateBuilder { self.args.dry_run = dry_run; self } + + pub fn json(mut self, json: bool) -> Self { + self.args.json = json; + self + } } impl IntoFuture for UpdateBuilder { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 52e3b9a25..23b9ed8d9 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -376,6 +376,7 @@ impl PixiControl { no_install: true, dry_run: false, specs: Default::default(), + json: false, }, } }