Skip to content

Commit 6a46532

Browse files
authored
feat: allow passing pypi paths (#572)
Allow passing paths as pypi package locations instead of just urls.
1 parent 3c457aa commit 6a46532

35 files changed

+1429
-509
lines changed

crates/rattler_lock/src/lib.rs

+11-8
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ mod conda;
8181
mod hash;
8282
mod parse;
8383
mod pypi;
84+
mod url_or_path;
8485
mod utils;
8586

8687
pub use builder::LockFileBuilder;
@@ -89,6 +90,7 @@ pub use conda::{CondaPackageData, ConversionError};
8990
pub use hash::PackageHashes;
9091
pub use parse::ParseCondaLockError;
9192
pub use pypi::{PypiPackageData, PypiPackageEnvironmentData};
93+
pub use url_or_path::UrlOrPath;
9294

9395
/// The name of the default environment in a [`LockFile`]. This is the environment name that is used
9496
/// when no explicit environment name is specified.
@@ -444,11 +446,11 @@ impl Package {
444446
}
445447
}
446448

447-
/// Returns the URL of the package
448-
pub fn url(&self) -> &Url {
449+
/// Returns the URL or relative path to the package
450+
pub fn url_or_path(&self) -> Cow<'_, UrlOrPath> {
449451
match self {
450-
Package::Conda(value) => value.url(),
451-
Package::Pypi(value) => value.url(),
452+
Self::Conda(value) => Cow::Owned(UrlOrPath::Url(value.url().clone())),
453+
Self::Pypi(value) => Cow::Borrowed(value.url()),
452454
}
453455
}
454456
}
@@ -545,8 +547,8 @@ impl PypiPackage {
545547
}
546548

547549
/// Returns the URL of the package
548-
pub fn url(&self) -> &Url {
549-
&self.package_data().url
550+
pub fn url(&self) -> &UrlOrPath {
551+
&self.package_data().url_or_path
550552
}
551553

552554
/// Returns the extras enabled for this package
@@ -586,6 +588,7 @@ mod test {
586588
#[case("v4/python-lock.yml")]
587589
#[case("v4/pypi-matplotlib-lock.yml")]
588590
#[case("v4/turtlesim-lock.yml")]
591+
#[case("v4/path-based-lock.yml")]
589592
fn test_parse(#[case] file_name: &str) {
590593
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
591594
.join("../../test-data/conda-lock")
@@ -608,15 +611,15 @@ mod test {
608611
.unwrap()
609612
.packages(Platform::Linux64)
610613
.unwrap()
611-
.map(|p| p.url().clone())
614+
.map(|p| p.url_or_path().into_owned())
612615
.collect::<Vec<_>>());
613616

614617
insta::assert_yaml_snapshot!(conda_lock
615618
.environment(DEFAULT_ENVIRONMENT_NAME)
616619
.unwrap()
617620
.packages(Platform::Osx64)
618621
.unwrap()
619-
.map(|p| p.url().clone())
622+
.map(|p| p.url_or_path().into_owned())
620623
.collect::<Vec<_>>());
621624
}
622625
}

crates/rattler_lock/src/parse/deserialize.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::utils::serde::RawCondaPackageData;
22
use crate::{
33
Channel, CondaPackageData, EnvironmentData, EnvironmentPackageData, LockFile, LockFileInner,
4-
ParseCondaLockError, PypiPackageData, PypiPackageEnvironmentData,
4+
ParseCondaLockError, PypiPackageData, PypiPackageEnvironmentData, UrlOrPath,
55
};
66
use fxhash::FxHashMap;
77
use indexmap::IndexSet;
@@ -40,7 +40,7 @@ enum DeserializablePackageSelector {
4040
conda: Url,
4141
},
4242
Pypi {
43-
pypi: Url,
43+
pypi: UrlOrPath,
4444
#[serde(flatten)]
4545
runtime: DeserializablePypiPackageEnvironmentData,
4646
},
@@ -81,7 +81,7 @@ pub fn parse_from_document(document: Value) -> Result<LockFile, ParseCondaLockEr
8181
let pypi_url_lookup = pypi_packages
8282
.iter()
8383
.enumerate()
84-
.map(|(idx, p)| (&p.url, idx))
84+
.map(|(idx, p)| (&p.url_or_path, idx))
8585
.collect::<FxHashMap<_, _>>();
8686
let mut pypi_runtime_lookup = IndexSet::new();
8787

@@ -107,7 +107,7 @@ pub fn parse_from_document(document: Value) -> Result<LockFile, ParseCondaLockEr
107107
ParseCondaLockError::MissingPackage(
108108
name.clone(),
109109
platform,
110-
conda,
110+
UrlOrPath::Url(conda),
111111
)
112112
})?,
113113
)

crates/rattler_lock/src/parse/mod.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ mod deserialize;
22
mod serialize;
33
mod v3;
44

5-
use super::LockFile;
5+
use super::{LockFile, UrlOrPath};
66
use rattler_conda_types::Platform;
77
use serde::de::Error;
88
use serde_yaml::Value;
99
use std::str::FromStr;
10-
use url::Url;
1110
use v3::parse_v3_or_lower;
1211

1312
// Version 2: dependencies are now arrays instead of maps
@@ -31,7 +30,7 @@ pub enum ParseCondaLockError {
3130
},
3231

3332
#[error("environment {0} and platform {1} refers to a package that does not exist: {2}")]
34-
MissingPackage(String, Platform, Url),
33+
MissingPackage(String, Platform, UrlOrPath),
3534

3635
#[error(transparent)]
3736
InvalidPypiPackageName(#[from] pep508_rs::InvalidNameError),

crates/rattler_lock/src/parse/serialize.rs

+43-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use super::FILE_VERSION;
22
use crate::utils::serde::RawCondaPackageData;
3-
use crate::{Channel, EnvironmentPackageData, LockFile, PypiPackageData};
3+
use crate::{Channel, EnvironmentPackageData, LockFile, PypiPackageData, UrlOrPath};
44
use itertools::Itertools;
55
use pep508_rs::ExtraName;
66
use rattler_conda_types::Platform;
77
use serde::{Serialize, Serializer};
8+
use std::borrow::Cow;
89
use std::collections::{BTreeSet, HashSet};
910
use std::{cmp::Ordering, collections::BTreeMap};
1011
use url::Url;
@@ -37,17 +38,19 @@ enum SerializablePackageSelector<'a> {
3738
conda: &'a Url,
3839
},
3940
Pypi {
40-
pypi: &'a Url,
41+
pypi: &'a UrlOrPath,
4142
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
4243
extras: &'a BTreeSet<ExtraName>,
4344
},
4445
}
4546

4647
impl<'a> SerializablePackageSelector<'a> {
47-
fn url(&self) -> &Url {
48+
fn url(&self) -> Cow<'_, UrlOrPath> {
4849
match self {
49-
SerializablePackageSelector::Conda { conda } => conda,
50-
SerializablePackageSelector::Pypi { pypi, .. } => pypi,
50+
SerializablePackageSelector::Conda { conda } => {
51+
Cow::Owned(UrlOrPath::Url((*conda).clone()))
52+
}
53+
SerializablePackageSelector::Pypi { pypi, .. } => Cow::Borrowed(pypi),
5154
}
5255
}
5356
}
@@ -78,34 +81,41 @@ impl<'a> Ord for SerializablePackageSelector<'a> {
7881
(
7982
SerializablePackageSelector::Conda { conda: a },
8083
SerializablePackageSelector::Conda { conda: b },
81-
)
82-
| (
84+
) => compare_url_by_filename(a, b),
85+
(
8386
SerializablePackageSelector::Pypi { pypi: a, .. },
8487
SerializablePackageSelector::Pypi { pypi: b, .. },
85-
) => {
86-
// First sort packages just by their filename. Since most of the time the urls end
87-
// in the packages filename this causes the urls to be sorted by package name.
88-
if let (Some(a), Some(b)) = (
89-
a.path_segments()
90-
.and_then(Iterator::last)
91-
.map(str::to_lowercase),
92-
b.path_segments()
93-
.and_then(Iterator::last)
94-
.map(str::to_lowercase),
95-
) {
96-
match a.cmp(&b) {
97-
Ordering::Equal => {}
98-
ordering => return ordering,
99-
}
100-
}
101-
102-
// Otherwise just sort by their full URL
103-
a.cmp(b)
104-
}
88+
) => match (a, b) {
89+
(UrlOrPath::Url(a), UrlOrPath::Url(b)) => compare_url_by_filename(a, b),
90+
(UrlOrPath::Url(_), UrlOrPath::Path(_)) => Ordering::Less,
91+
(UrlOrPath::Path(_), UrlOrPath::Url(_)) => Ordering::Greater,
92+
(UrlOrPath::Path(a), UrlOrPath::Path(b)) => a.cmp(b),
93+
},
10594
}
10695
}
10796
}
10897

98+
/// First sort packages just by their filename. Since most of the time the urls end
99+
/// in the packages filename this causes the urls to be sorted by package name.
100+
fn compare_url_by_filename(a: &Url, b: &Url) -> Ordering {
101+
if let (Some(a), Some(b)) = (
102+
a.path_segments()
103+
.and_then(Iterator::last)
104+
.map(str::to_lowercase),
105+
b.path_segments()
106+
.and_then(Iterator::last)
107+
.map(str::to_lowercase),
108+
) {
109+
match a.cmp(&b) {
110+
Ordering::Equal => {}
111+
ordering => return ordering,
112+
}
113+
}
114+
115+
// Otherwise just sort by their full URL
116+
a.cmp(b)
117+
}
118+
109119
impl<'a> SerializablePackageData<'a> {
110120
fn source_name(&self) -> &str {
111121
match self {
@@ -114,10 +124,12 @@ impl<'a> SerializablePackageData<'a> {
114124
}
115125
}
116126

117-
fn url(&self) -> &Url {
127+
fn url(&self) -> Cow<'_, UrlOrPath> {
118128
match self {
119-
SerializablePackageData::Conda(p) => &p.url,
120-
SerializablePackageData::Pypi(p) => &p.url,
129+
SerializablePackageData::Conda(p) => {
130+
Cow::Owned(UrlOrPath::Url(p.url.clone().into_owned()))
131+
}
132+
SerializablePackageData::Pypi(p) => Cow::Borrowed(&p.url_or_path),
121133
}
122134
}
123135
}
@@ -197,7 +209,7 @@ impl Serialize for LockFile {
197209
.pypi_environment_package_datas
198210
[pypi_runtime_index];
199211
SerializablePackageSelector::Pypi {
200-
pypi: &pypi_package.url,
212+
pypi: &pypi_package.url_or_path,
201213
extras: &pypi_runtime.extras,
202214
}
203215
}

crates/rattler_lock/src/parse/v3.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
use super::ParseCondaLockError;
44
use crate::{
55
Channel, CondaPackageData, EnvironmentData, EnvironmentPackageData, LockFile, LockFileInner,
6-
PackageHashes, PypiPackageData, PypiPackageEnvironmentData, DEFAULT_ENVIRONMENT_NAME,
6+
PackageHashes, PypiPackageData, PypiPackageEnvironmentData, UrlOrPath,
7+
DEFAULT_ENVIRONMENT_NAME,
78
};
89
use fxhash::FxHashMap;
910
use indexmap::IndexSet;
@@ -182,7 +183,7 @@ pub fn parse_v3_or_lower(document: serde_yaml::Value) -> Result<LockFile, ParseC
182183
version: pkg.version,
183184
requires_dist: pkg.requires_dist,
184185
requires_python: pkg.requires_python,
185-
url: pkg.url,
186+
url_or_path: UrlOrPath::Url(pkg.url),
186187
hash: pkg.hash,
187188
})
188189
.0;

crates/rattler_lock/src/pypi.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use crate::PackageHashes;
1+
use crate::{PackageHashes, UrlOrPath};
22
use pep440_rs::VersionSpecifiers;
33
use pep508_rs::{ExtraName, PackageName, Requirement};
44
use serde::{Deserialize, Serialize};
55
use serde_with::{serde_as, skip_serializing_none};
66
use std::cmp::Ordering;
77
use std::collections::BTreeSet;
8-
use url::Url;
98

109
/// A pinned Pypi package
1110
#[serde_as]
@@ -19,7 +18,8 @@ pub struct PypiPackageData {
1918
pub version: pep440_rs::Version,
2019

2120
/// The URL that points to where the artifact can be downloaded from.
22-
pub url: Url,
21+
#[serde(with = "crate::utils::serde::url_or_path", flatten)]
22+
pub url_or_path: UrlOrPath,
2323

2424
/// Hashes of the file pointed to by `url`.
2525
#[serde(flatten)]

0 commit comments

Comments
 (0)