From 60069f8029f2c52630e81685fcc566278b300a64 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Fri, 15 Nov 2024 14:15:17 +0100 Subject: [PATCH] refactor: python API for lock files --- crates/rattler-bin/src/commands/create.rs | 11 +-- crates/rattler/src/lib.rs | 2 +- .../rattler_conda_types/src/match_spec/mod.rs | 2 +- .../rattler_conda_types/src/repo_data/mod.rs | 2 +- .../src/repo_data_record.rs | 16 ++-- crates/rattler_lock/src/builder.rs | 1 + crates/rattler_lock/src/conda.rs | 8 +- crates/rattler_lock/src/lib.rs | 36 ++++++++- .../src/parse/models/v5/conda_package_data.rs | 3 +- crates/rattler_lock/src/parse/serialize.rs | 5 +- .../src/gateway/sharded_subdir/mod.rs | 16 ++-- .../src/sparse/mod.rs | 3 +- ...tivation__tests__after_activation.snap.new | 9 +++ crates/rattler_solve/src/libsolv_c/mod.rs | 4 +- crates/rattler_solve/src/resolvo/mod.rs | 6 +- crates/rattler_solve/tests/backends.rs | 10 +-- py-rattler/Cargo.lock | 7 -- py-rattler/Cargo.toml | 2 - py-rattler/rattler/__init__.py | 8 +- py-rattler/rattler/lock/__init__.py | 8 +- py-rattler/rattler/lock/environment.py | 22 ++---- py-rattler/rattler/lock/package.py | 53 ++++++++----- py-rattler/rattler/repo_data/record.py | 2 +- py-rattler/src/lock/mod.rs | 79 ++++++------------- py-rattler/tests/unit/test_prefix_record.py | 4 +- 25 files changed, 172 insertions(+), 147 deletions(-) create mode 100644 crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__after_activation.snap.new diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 9d3ccc589..346e46d6f 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -273,7 +273,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { if transaction.operations.is_empty() { println!("No operations necessary"); } else { - print_transaction(&transaction, &channel_config); + print_transaction(&transaction); } return Ok(()); @@ -304,20 +304,17 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { console::style(console::Emoji("✔", "")).green(), install_start.elapsed() ); - print_transaction(&result.transaction, &channel_config); + print_transaction(&result.transaction); } Ok(()) } /// Prints the operations of the transaction to the console. -fn print_transaction( - transaction: &Transaction, - channel_config: &ChannelConfig, -) { +fn print_transaction(transaction: &Transaction) { let format_record = |r: &RepoDataRecord| { let direct_url_print = if let Some(channel) = &r.channel { - channel_config.canonical_name(channel.as_ref()) + channel.clone() } else { String::new() }; diff --git a/crates/rattler/src/lib.rs b/crates/rattler/src/lib.rs index 938cc7d62..48638ae69 100644 --- a/crates/rattler/src/lib.rs +++ b/crates/rattler/src/lib.rs @@ -78,6 +78,6 @@ pub(crate) fn get_repodata_record(package_path: impl AsRef) -> .unwrap() .to_string(), url: url::Url::from_file_path(package_path).unwrap(), - channel: "test".to_string(), + channel: Some(String::from("test")), } } diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index 58071da7d..c50b9d8be 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -676,7 +676,7 @@ mod tests { ), file_name: String::from("mamba-1.0-py37_0"), url: url::Url::parse("https://mamba.io/mamba-1.0-py37_0.conda").unwrap(), - channel: String::from("mamba"), + channel: Some(String::from("mamba")), }; let package_record = repodata_record.clone().package_record; diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index 982f7b78d..9affc36ea 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -240,7 +240,7 @@ impl RepoData { base_url.as_deref(), &filename, ), - channel: Some(channel.base_url.clone()), + channel: Some(channel.base_url.as_str().to_string()), package_record, file_name: filename, }); diff --git a/crates/rattler_conda_types/src/repo_data_record.rs b/crates/rattler_conda_types/src/repo_data_record.rs index ff4afc443..9d12937f5 100644 --- a/crates/rattler_conda_types/src/repo_data_record.rs +++ b/crates/rattler_conda_types/src/repo_data_record.rs @@ -1,13 +1,11 @@ //! Defines the `[RepoDataRecord]` struct. +use crate::PackageRecord; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{ChannelUrl, PackageRecord}; - -/// Information about a package from repodata. It includes a -/// [`crate::PackageRecord`] but it also stores the source of the data (like the -/// url and the channel). +/// Information about a package from repodata. It includes a [`crate::PackageRecord`] but it also stores +/// the source of the data (like the url and the channel). #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Hash)] pub struct RepoDataRecord { /// The data stored in the repodata.json. @@ -21,9 +19,11 @@ pub struct RepoDataRecord { /// The canonical URL from where to get this package. pub url: Url, - /// The channel that contains the package. This might be `None` for a - /// package that does not come from a channel. - pub channel: Option, + /// String representation of the channel where the package comes from. This could be a URL but + /// it could also be a channel name. Personally I would always add the complete URL here to be + /// explicit about where the package came from. + /// TODO: Refactor this into `Source` which can be a "name", "channelurl", or "direct url". + pub channel: Option, } impl AsRef for RepoDataRecord { diff --git a/crates/rattler_lock/src/builder.rs b/crates/rattler_lock/src/builder.rs index dc765de5e..7386593bc 100644 --- a/crates/rattler_lock/src/builder.rs +++ b/crates/rattler_lock/src/builder.rs @@ -18,6 +18,7 @@ use crate::{ /// Information about a single locked package in an environment. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum LockedPackage { /// A conda package Conda(CondaPackageData), diff --git a/crates/rattler_lock/src/conda.rs b/crates/rattler_lock/src/conda.rs index 170986abe..b2b7d7fb7 100644 --- a/crates/rattler_lock/src/conda.rs +++ b/crates/rattler_lock/src/conda.rs @@ -2,6 +2,7 @@ use std::{cmp::Ordering, hash::Hash}; use rattler_conda_types::{ChannelUrl, PackageRecord, RepoDataRecord}; use rattler_digest::Sha256Hash; +use url::Url; use crate::UrlOrPath; @@ -148,7 +149,10 @@ impl From for CondaPackageData { Self::Binary(CondaBinaryData { package_record: value.package_record, file_name: value.file_name, - channel: value.channel, + channel: value + .channel + .and_then(|channel| Url::parse(&channel).ok()) + .map(Into::into), location, }) } @@ -170,7 +174,7 @@ impl TryFrom for RepoDataRecord { package_record: value.package_record, file_name: value.file_name, url: value.location.try_into_url()?, - channel: value.channel, + channel: value.channel.map(|channel| channel.to_string()), }) } } diff --git a/crates/rattler_lock/src/lib.rs b/crates/rattler_lock/src/lib.rs index 60e0bae3a..94fe8bb61 100644 --- a/crates/rattler_lock/src/lib.rs +++ b/crates/rattler_lock/src/lib.rs @@ -237,7 +237,7 @@ impl LockFile { /// Information about a specific environment in the lock-file. #[derive(Clone, Copy)] pub struct Environment<'lock> { - inner: &'lock LockFileInner, + inner: &'lock Arc, index: usize, } @@ -415,6 +415,40 @@ impl<'lock> Environment<'lock> { self.packages(platform) .map(|pkgs| pkgs.filter_map(LockedPackageRef::as_pypi)) } + + /// Creates a [`OwnedEnvironment`] from this environment. + pub fn to_owned(self) -> OwnedEnvironment { + OwnedEnvironment { + inner: self.inner.clone(), + index: self.index, + } + } +} + +/// An owned version of an [`Environment`]. +/// +/// Use [`OwnedEnvironment::as_ref`] to get a reference to the environment data. +#[derive(Clone)] +pub struct OwnedEnvironment { + inner: Arc, + index: usize, +} + +impl OwnedEnvironment { + /// Returns a reference to the environment data. + pub fn as_ref(&self) -> Environment<'_> { + Environment { + inner: &self.inner, + index: self.index, + } + } + + /// Returns the lock-file this environment is part of. + pub fn lock_file(&self) -> LockFile { + LockFile { + inner: self.inner.clone(), + } + } } /// Data related to a single locked package in an [`Environment`]. diff --git a/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs b/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs index 087496e6f..6b3f7b27d 100644 --- a/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs +++ b/crates/rattler_lock/src/parse/models/v5/conda_package_data.rs @@ -62,7 +62,7 @@ pub(crate) struct CondaPackageDataModel<'a> { pub platform: Cow<'a, Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub channel: Cow<'a, Option>, + pub channel: Cow<'a, Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub features: Cow<'a, Option>, @@ -141,6 +141,7 @@ impl<'a> From> for CondaPackageData { channel: value .channel .into_owned() + .map(ChannelUrl::from) .or_else(|| derive_channel_from_location(&location)), file_name, location, diff --git a/crates/rattler_lock/src/parse/serialize.rs b/crates/rattler_lock/src/parse/serialize.rs index d457c0fe8..13d52aa25 100644 --- a/crates/rattler_lock/src/parse/serialize.rs +++ b/crates/rattler_lock/src/parse/serialize.rs @@ -14,9 +14,8 @@ use url::Url; use crate::{ file_format_version::FileFormatVersion, parse::{models::v6, V6}, - Channel, CondaPackageData, EnvironmentData, EnvironmentPackageData, LockFile, - LockFileInner, PypiIndexes, PypiPackageData, PypiPackageEnvironmentData, - UrlOrPath, + Channel, CondaPackageData, EnvironmentData, EnvironmentPackageData, LockFile, LockFileInner, + PypiIndexes, PypiPackageData, PypiPackageEnvironmentData, UrlOrPath, }; #[serde_as] diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs index a184a3cad..74f3a998d 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -1,20 +1,20 @@ use std::{borrow::Cow, io::Write, path::PathBuf, sync::Arc}; +use crate::{ + fetch::{CacheAction, FetchRepoDataError}, + gateway::{error::SubdirNotFoundError, subdir::SubdirClient}, + reporter::ResponseReporterExt, + GatewayError, Reporter, +}; use http::{header::CACHE_CONTROL, HeaderValue, StatusCode}; use rattler_conda_types::{ Channel, ChannelUrl, PackageName, RepoDataRecord, Shard, ShardedRepodata, }; +use rattler_redaction::Redact; use reqwest_middleware::ClientWithMiddleware; use simple_spawn_blocking::tokio::run_blocking_task; use url::Url; -use crate::{ - fetch::{CacheAction, FetchRepoDataError}, - gateway::{error::SubdirNotFoundError, subdir::SubdirClient}, - reporter::ResponseReporterExt, - GatewayError, Reporter, -}; - mod index; pub struct ShardedSubdir { @@ -293,7 +293,7 @@ async fn parse_records + Send + 'static>( url: base_url .join(&file_name) .expect("filename is not a valid url"), - channel: Some(channel_base_url.clone()), + channel: Some(channel_base_url.url().clone().redact().to_string()), package_record, file_name, }) diff --git a/crates/rattler_repodata_gateway/src/sparse/mod.rs b/crates/rattler_repodata_gateway/src/sparse/mod.rs index 407158bfd..b7e69473d 100644 --- a/crates/rattler_repodata_gateway/src/sparse/mod.rs +++ b/crates/rattler_repodata_gateway/src/sparse/mod.rs @@ -17,6 +17,7 @@ use itertools::Itertools; use rattler_conda_types::{ compute_package_url, Channel, ChannelInfo, PackageName, PackageRecord, RepoDataRecord, }; +use rattler_redaction::Redact; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, @@ -315,7 +316,7 @@ fn parse_records<'i>( base_url, key.filename, ), - channel: Some(channel_name.clone()), + channel: Some(channel_name.url().clone().redact().to_string()), package_record, file_name: key.filename.to_owned(), }); diff --git a/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__after_activation.snap.new b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__after_activation.snap.new new file mode 100644 index 000000000..6fdecfce3 --- /dev/null +++ b/crates/rattler_shell/src/snapshots/rattler_shell__activation__tests__after_activation.snap.new @@ -0,0 +1,9 @@ +--- +source: crates/rattler_shell/src/activation.rs +assertion_line: 764 +expression: env_diff +snapshot_kind: text +--- +PKG1: "Hello, world!" +PKG2: "Hello, world!" +STATE: "Hello, world!" diff --git a/crates/rattler_solve/src/libsolv_c/mod.rs b/crates/rattler_solve/src/libsolv_c/mod.rs index 76868f5b0..997fc3faa 100644 --- a/crates/rattler_solve/src/libsolv_c/mod.rs +++ b/crates/rattler_solve/src/libsolv_c/mod.rs @@ -10,7 +10,7 @@ pub use input::cache_repodata; use input::{add_repodata_records, add_solv_file, add_virtual_packages}; pub use libc_byte_slice::LibcByteSlice; use output::get_required_packages; -use rattler_conda_types::{ChannelUrl, MatchSpec, NamelessMatchSpec, RepoDataRecord}; +use rattler_conda_types::{MatchSpec, NamelessMatchSpec, RepoDataRecord}; use wrapper::{ flags::SolverFlag, pool::{Pool, Verbosity}, @@ -179,7 +179,7 @@ impl super::SolverImpl for Solver { }; let repo = ManuallyDrop::new(Repo::new( &pool, - channel_name.as_ref().map_or("", ChannelUrl::as_str), + channel_name.as_ref().map_or("", String::as_str), priority, )); diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index 559823ab0..413d599de 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -13,7 +13,7 @@ use chrono::{DateTime, Utc}; use conda_sorting::SolvableSorter; use itertools::Itertools; use rattler_conda_types::{ - package::ArchiveType, ChannelUrl, GenericVirtualPackage, MatchSpec, Matches, NamelessMatchSpec, + package::ArchiveType, GenericVirtualPackage, MatchSpec, Matches, NamelessMatchSpec, PackageName, PackageRecord, ParseMatchSpecError, ParseStrictness, RepoDataRecord, }; use resolvo::{ @@ -221,7 +221,7 @@ impl<'a> CondaDependencyProvider<'a> { .collect::>(); // Hashmap that maps the package name to the channel it was first found in. - let mut package_name_found_in_channel = HashMap::>::new(); + let mut package_name_found_in_channel = HashMap::>::new(); // Add additional records for repo_data in repodata { @@ -324,7 +324,7 @@ impl<'a> CondaDependencyProvider<'a> { }) { // Check if the spec has a channel, and compare it to the repodata channel if let Some(spec_channel) = &spec.channel { - if record.channel.as_ref() != Some(&spec_channel.base_url) { + if record.channel.as_ref() != Some(&spec_channel.canonical_name()) { tracing::debug!("Ignoring {} {} because it was not requested from that channel.", &record.package_record.name.as_normalized(), match &record.channel { Some(channel) => format!("from {}", &channel), None => "without a channel".to_string(), diff --git a/crates/rattler_solve/tests/backends.rs b/crates/rattler_solve/tests/backends.rs index 2d0430df0..bd50aa016 100644 --- a/crates/rattler_solve/tests/backends.rs +++ b/crates/rattler_solve/tests/backends.rs @@ -84,7 +84,7 @@ fn installed_package( ) -> RepoDataRecord { RepoDataRecord { url: Url::from_str("http://example.com").unwrap(), - channel: channel.to_string(), + channel: Some(channel.to_string()), file_name: "dummy-filename".to_string(), package_record: PackageRecord { name: name.parse().unwrap(), @@ -352,7 +352,7 @@ macro_rules! solver_backend_tests { "https://conda.anaconda.org/conda-forge/linux-64/foo-3.0.2-py36h1af98f8_3.conda", info.url.to_string() ); - assert_eq!("https://conda.anaconda.org/conda-forge/", info.channel); + assert_eq!(Some("https://conda.anaconda.org/conda-forge/"), info.channel.as_deref()); assert_eq!("foo", info.package_record.name.as_normalized()); assert_eq!("linux-64", info.package_record.subdir); assert_eq!("3.0.2", info.package_record.version.to_string()); @@ -867,7 +867,7 @@ mod resolvo { // Mocking the rest of the fields file_name: url_str.to_string(), url: url.clone(), - channel: "".to_string(), + channel: None, }]; // Completely clean solver task, except for the specs and RepoData @@ -900,7 +900,7 @@ mod resolvo { package_record, file_name: url_str.to_string(), url: Url::from_str("https://false.dont").unwrap(), - channel: "".to_string(), + channel: None, }]; // Completely clean solver task, except for the specs and RepoData @@ -1155,7 +1155,7 @@ fn solve_to_get_channel_of_spec( let record = result.iter().find(|record| { record.package_record.name.as_normalized() == spec.name.as_ref().unwrap().as_normalized() }); - assert_eq!(record.unwrap().channel, expected_channel.to_string()); + assert_eq!(record.unwrap().channel, Some(expected_channel.to_string())); } #[test] diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 9d1948ac1..f6d208dc8 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -2494,7 +2494,6 @@ dependencies = [ "rattler_virtual_packages", "reqwest", "reqwest-middleware", - "self_cell", "serde_json", "thiserror", "tokio", @@ -3365,12 +3364,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" - [[package]] name = "serde" version = "1.0.214" diff --git a/py-rattler/Cargo.toml b/py-rattler/Cargo.toml index ef769d26c..63575313e 100644 --- a/py-rattler/Cargo.toml +++ b/py-rattler/Cargo.toml @@ -20,8 +20,6 @@ anyhow = "1.0.92" chrono = { version = "0.4" } futures = "0.3.31" -self_cell = "1.0.4" - rattler = { path = "../crates/rattler", default-features = false, features = ["indicatif"] } rattler_repodata_gateway = { path = "../crates/rattler_repodata_gateway", default-features = false, features = [ "sparse", diff --git a/py-rattler/rattler/__init__.py b/py-rattler/rattler/__init__.py index 3ea2b422b..3c5dc8a34 100644 --- a/py-rattler/rattler/__init__.py +++ b/py-rattler/rattler/__init__.py @@ -38,7 +38,7 @@ CondaLockedSourcePackage, CondaLockedBinaryPackage, CondaLockedPackage, - PypiLockedPackage + PypiLockedPackage, ) from rattler.solver import solve, solve_with_sparse_repodata @@ -74,8 +74,10 @@ "LockChannel", "PackageHashes", "LockedPackage", - "PypiPackageData", - "PypiPackageEnvironmentData", + "CondaLockedSourcePackage", + "CondaLockedBinaryPackage", + "CondaLockedPackage", + "PypiLockedPackage", "solve", "solve_with_sparse_repodata", "Platform", diff --git a/py-rattler/rattler/lock/__init__.py b/py-rattler/rattler/lock/__init__.py index a1abde22c..d29320201 100644 --- a/py-rattler/rattler/lock/__init__.py +++ b/py-rattler/rattler/lock/__init__.py @@ -2,7 +2,13 @@ from rattler.lock.environment import Environment from rattler.lock.channel import LockChannel from rattler.lock.hash import PackageHashes -from rattler.lock.package import LockedPackage, PypiLockedPackage, CondaLockedPackage, CondaLockedBinaryPackage, CondaLockedSourcePackage +from rattler.lock.package import ( + LockedPackage, + PypiLockedPackage, + CondaLockedPackage, + CondaLockedBinaryPackage, + CondaLockedSourcePackage, +) __all__ = [ "LockFile", diff --git a/py-rattler/rattler/lock/environment.py b/py-rattler/rattler/lock/environment.py index 2513583ef..7b85ca083 100644 --- a/py-rattler/rattler/lock/environment.py +++ b/py-rattler/rattler/lock/environment.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional from rattler.lock.channel import LockChannel from rattler.lock.package import LockedPackage, PypiLockedPackage from rattler.platform.platform import Platform @@ -72,7 +72,7 @@ def packages(self, platform: Platform) -> Optional[List[LockedPackage]]: >>> lock_file = LockFile.from_path("../test-data/test.lock") >>> env = lock_file.default_environment() >>> env.packages(Platform("osx-arm64"))[0] - CondaLockedBinaryPackage() + CondaLockedBinaryPackage(name='tzdata',location='https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda') >>> ``` """ @@ -103,7 +103,7 @@ def packages_by_platform(self) -> Dict[Platform, List[LockedPackage]]: def pypi_packages( self, - ) -> Dict[Platform, List[Tuple[PypiLockedPackage]]]: + ) -> Dict[Platform, List[PypiLockedPackage]]: """ Returns all pypi packages for all platforms. @@ -115,13 +115,12 @@ def pypi_packages( >>> env = lock_file.default_environment() >>> pypi_packages = env.pypi_packages() >>> pypi_packages[Platform("osx-arm64")][0] - PypiLockedPackage() + PypiLockedPackage(name='charset-normalizer',location='https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl#sha256=55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6') >>> ``` """ return { - Platform._from_py_platform(platform): - [PypiLockedPackage._from_py_locked_package(pypi) for pypi in pypi_tup] + Platform._from_py_platform(platform): [PypiLockedPackage._from_py_locked_package(pypi) for pypi in pypi_tup] for (platform, pypi_tup) in self._env.pypi_packages().items() } @@ -169,9 +168,7 @@ def conda_repodata_records_for_platform(self, platform: Platform) -> Optional[Li return [RepoDataRecord._from_py_record(r) for r in records] return None - def pypi_packages_for_platform( - self, platform: Platform - ) -> Optional[List[Tuple[PypiLockedPackage]]]: + def pypi_packages_for_platform(self, platform: Platform) -> Optional[List[PypiLockedPackage]]: """ Returns all the pypi packages and their associated environment data for the specified platform. Returns `None` if the platform is not defined for this environment. @@ -186,15 +183,12 @@ def pypi_packages_for_platform( >>> osx_pypi_pkgs [...] >>> osx_pypi_pkgs[0] - PypiLockedPackage() + PypiLockedPackage(name='charset-normalizer',location='https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl#sha256=55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6') >>> ``` """ if data := self._env.pypi_packages_for_platform(platform._inner): - return [ - PypiLockedPackage._from_py_locked_package(pkg) - for pkg in data - ] + return [PypiLockedPackage._from_py_locked_package(pkg) for pkg in data] return None @classmethod diff --git a/py-rattler/rattler/lock/package.py b/py-rattler/rattler/lock/package.py index 85b33e65a..bf4b9a97e 100644 --- a/py-rattler/rattler/lock/package.py +++ b/py-rattler/rattler/lock/package.py @@ -1,6 +1,6 @@ from __future__ import annotations from abc import ABC -from typing import Optional, Set +from typing import Optional, Set, List from rattler import PackageRecord, Version, RepoDataRecord @@ -9,6 +9,10 @@ class LockedPackage(ABC): + """ + Base class for any package in a lock file. + """ + _package: PyLockedPackage @property @@ -75,16 +79,14 @@ def __repr__(self) -> str: """ Returns a representation of the LockedPackage. """ - return (f"{type(self).__name__}(" - f"name={self.name!r}," - f"location={self.location!r}" - ")") + return f"{type(self).__name__}(" f"name={self.name!r}," f"location={self.location!r}" ")" @classmethod def _from_py_locked_package(cls, py_pkg: PyLockedPackage) -> LockedPackage: """ Construct Rattler LockedPackage from FFI PyLockedPackage object. """ + pkg: LockedPackage if py_pkg.is_conda_binary: pkg = CondaLockedBinaryPackage.__new__(CondaLockedBinaryPackage) elif py_pkg.is_conda_source: @@ -93,13 +95,17 @@ def _from_py_locked_package(cls, py_pkg: PyLockedPackage) -> LockedPackage: pkg = PypiLockedPackage.__new__(PypiLockedPackage) else: raise TypeError( - "Cannot create LockedPackage from PyLockedPackage, the type of the package is not supported.") + "Cannot create LockedPackage from PyLockedPackage, the type of the package is not supported." + ) pkg._package = py_pkg return pkg class CondaLockedPackage(LockedPackage, ABC): + """ + A locked conda package in a lock file. + """ @property def package_record(self) -> PackageRecord: @@ -129,6 +135,9 @@ def version(self) -> Version: class PypiLockedPackage(LockedPackage): + """ + A locked PyPI package in a lock file. + """ @property def version(self) -> str: @@ -237,31 +246,35 @@ def satisfies(self, spec: str) -> bool: """ return self._package.pypi_satisfies(spec) - def __repr__(self) -> str: + @classmethod + def _from_py_locked_package(cls, py_pkg: PyLockedPackage) -> PypiLockedPackage: """ - Returns a representation of the LockedPackage. + Construct Rattler LockedPackage from FFI PyLockedPackage object. """ - return "PypiLockedPackage()" + if py_pkg.is_pypi: + pkg = PypiLockedPackage.__new__(PypiLockedPackage) + else: + raise TypeError( + "Cannot create PypiLockedPackage from PyLockedPackage, the type of the package is not supported." + ) + + pkg._package = py_pkg + return pkg class CondaLockedSourcePackage(CondaLockedPackage): - def __repr__(self) -> str: - """ - Returns a representation of the LockedPackage. - """ - return "CondaLockedSourcePackage()" + """ + A locked conda source package in a lock file. + """ class CondaLockedBinaryPackage(CondaLockedPackage): + """ + A locked conda binary package in a lock file. + """ def repo_data_record(self) -> RepoDataRecord: """ Returns the metadata of the package as recorded in the lock-file including location information. """ return RepoDataRecord._from_py_record(self._package.repo_data_record) - - def __repr__(self) -> str: - """ - Returns a representation of the LockedPackage. - """ - return "CondaLockedBinaryPackage()" diff --git a/py-rattler/rattler/repo_data/record.py b/py-rattler/rattler/repo_data/record.py index 574b1c425..bf9164a61 100644 --- a/py-rattler/rattler/repo_data/record.py +++ b/py-rattler/rattler/repo_data/record.py @@ -68,7 +68,7 @@ def channel(self) -> str: ... "../test-data/conda-meta/libsqlite-3.40.0-hcfcfb64_0.json" ... ) >>> record.channel - 'https://conda.anaconda.org/conda-forge/win-64/' + 'https://conda.anaconda.org/conda-forge/win-64' >>> ``` """ diff --git a/py-rattler/src/lock/mod.rs b/py-rattler/src/lock/mod.rs index 3bc07129d..54d0a93b0 100644 --- a/py-rattler/src/lock/mod.rs +++ b/py-rattler/src/lock/mod.rs @@ -4,16 +4,14 @@ use pep508_rs::Requirement; use pyo3::{pyclass, pymethods, types::PyBytes, Bound, PyResult, Python}; use rattler_conda_types::RepoDataRecord; use rattler_lock::{ - Channel, CondaPackageData, Environment, LockFile, LockedPackage, PackageHashes, - PypiPackageData, PypiPackageEnvironmentData, DEFAULT_ENVIRONMENT_NAME, + Channel, CondaPackageData, Environment, LockFile, LockedPackage, OwnedEnvironment, + PackageHashes, PypiPackageData, PypiPackageEnvironmentData, DEFAULT_ENVIRONMENT_NAME, }; -use self_cell::self_cell; use std::{ collections::{BTreeSet, HashMap, HashSet}, path::PathBuf, str::FromStr, }; -use url::Url; /// Represents a lock-file for both Conda packages and Pypi packages. /// @@ -103,44 +101,25 @@ impl PyLockFile { } } -self_cell!( - struct EnvironmentRef { - owner: LockFile, - - #[covariant] - dependent: Environment, - } -); - #[pyclass] +#[derive(Clone)] pub struct PyEnvironment { - environment: EnvironmentRef, - name: String, -} - -impl Clone for PyEnvironment { - fn clone(&self) -> Self { - PyEnvironment::from_lock_file_and_name(self.environment.borrow_owner().clone(), &self.name) - .unwrap() - } + environment: OwnedEnvironment, } impl PyEnvironment { - fn as_ref(&self) -> &Environment<'_> { - self.environment.borrow_dependent() + fn as_ref(&self) -> Environment<'_> { + self.environment.as_ref() } pub fn from_lock_file_and_name(lock: LockFile, name: &str) -> PyResult { - let environment = EnvironmentRef::try_new(lock, move |lock| { - lock.environment(name) - .ok_or(PyRattlerError::EnvironmentCreationError( - "Environment creation failed.".into(), - )) - })?; - Ok(Self { - environment, - name: name.to_string(), - }) + let environment = lock + .environment(name) + .ok_or(PyRattlerError::EnvironmentCreationError( + "Environment creation failed.".into(), + ))? + .to_owned(); + Ok(Self { environment }) } } @@ -149,23 +128,17 @@ impl PyEnvironment { #[new] pub fn new(name: String, req: HashMap>) -> PyResult { let mut lock = LockFile::builder(); - let mut channels = HashSet::new(); - for records in req.values() { - for record in records { - if let Some(channel) = record - .try_as_repodata_record() - .ok() - .and_then(|r| r.channel.as_ref()) - { - channels.insert(Url::from(channel.clone())); - } - } - } + let channels = req + .values() + .flat_map(|records| { + records + .iter() + .filter_map(|r| r.channel().transpose()) + .collect::>>() + }) + .collect::>>()?; - lock.set_channels( - &name, - channels.into_iter().map(|url| Channel::from(url.as_str())), - ); + lock.set_channels(&name, channels); for (platform, records) in req { for record in records { @@ -231,8 +204,8 @@ impl PyEnvironment { let data = data_vec .map(|(pkg_data, pkg_env_data)| { PyLockedPackage::from(LockedPackage::Pypi( - pkg_data.clone().into(), - pkg_env_data.clone().into(), + pkg_data.clone(), + pkg_env_data.clone(), )) }) .collect::>(); @@ -404,7 +377,7 @@ impl PyLockedPackage { #[getter] pub fn pypi_version(&self) -> String { - self.as_pypi().version.to_string().into() + self.as_pypi().version.to_string() } /// Whether the package is installed in editable mode or not. diff --git a/py-rattler/tests/unit/test_prefix_record.py b/py-rattler/tests/unit/test_prefix_record.py index a94c37ac9..f2536e4ca 100644 --- a/py-rattler/tests/unit/test_prefix_record.py +++ b/py-rattler/tests/unit/test_prefix_record.py @@ -19,7 +19,7 @@ def test_load_prefix_record() -> None: assert r.arch == "x86_64" assert r.build == "h8ffe710_0" assert r.build_number == 0 - assert r.channel == "https://conda.anaconda.org/conda-forge/win-64/" + assert r.channel == "https://conda.anaconda.org/conda-forge/win-64" assert len(r.constrains) == 0 assert len(r.depends) == 2 assert str(r.extracted_package_dir) == "C:\\Users\\bas\\micromamba\\envs\\conda\\pkgs\\tk-8.6.12-h8ffe710_0" @@ -86,7 +86,7 @@ def test_create_prefix_record() -> None: ) assert repodata_record.url == "https://foobar.com/foobar.tar.bz2" - assert repodata_record.channel == "https://foobar.com/win-64/" + assert repodata_record.channel == "https://foobar.com/win-64" assert repodata_record.file_name == "foobar.tar.bz2" paths_data = PrefixPaths()