Skip to content

Commit

Permalink
fix: using file url as mapping (#1930)
Browse files Browse the repository at this point in the history
This fixes the issue reported by our user :
https://discord.com/channels/1082332781146800168/1276948298330275922/1276948298330275922


when using this mapping setup : 
```
[tool.pixi.project.conda-pypi-map]
conda-forge = "file:///path/to/parselmouth/conda-forge.json"
```

pixi will error out because we don't understand file url scheme.

```
  × failed to download pypi mapping from file:///path/to/parselmouth/conda-forge.json location
  ├─▶ builder error for url (file:///path/to/parselmouth/conda-forge.json)
  ╰─▶ URL scheme is not allowed
```

This PR aims to fix it.
  • Loading branch information
nichmor authored Sep 2, 2024
1 parent f248d78 commit 6fbb610
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 40 deletions.
31 changes: 23 additions & 8 deletions crates/pypi_mapping/src/custom_pypi_mapping.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
collections::{BTreeSet, HashMap},
path::Path,
sync::Arc,
};

Expand All @@ -13,13 +14,12 @@ use super::{
CustomMapping, PurlSource, Reporter,
};

pub async fn fetch_mapping_from_url<T>(
pub type CompressedMapping = HashMap<String, Option<String>>;

pub async fn fetch_mapping_from_url(
client: &ClientWithMiddleware,
url: &Url,
) -> miette::Result<T>
where
T: serde::de::DeserializeOwned,
{
) -> miette::Result<CompressedMapping> {
let response = client
.get(url.clone())
.send()
Expand All @@ -37,14 +37,29 @@ where
));
}

let mapping_by_name: T = response.json().await.into_diagnostic().context(format!(
let mapping_by_name = response.json().await.into_diagnostic().context(format!(
"failed to parse pypi name mapping located at {}. Please make sure that it's a valid json",
url
))?;

Ok(mapping_by_name)
}

pub fn fetch_mapping_from_path(path: &Path) -> miette::Result<CompressedMapping> {
let file = std::fs::File::open(path)
.into_diagnostic()
.context(format!("failed to open file {}", path.display()))?;
let reader = std::io::BufReader::new(file);
let mapping_by_name = serde_json::from_reader(reader)
.into_diagnostic()
.context(format!(
"failed to parse pypi name mapping located at {}. Please make sure that it's a valid json",
path.display()
))?;

Ok(mapping_by_name)
}

/// Amend the records with pypi purls if they are not present yet.
pub async fn amend_pypi_purls(
client: &ClientWithMiddleware,
Expand Down Expand Up @@ -99,7 +114,7 @@ pub async fn amend_pypi_purls(
/// the record refers to a conda-forge package.
fn amend_pypi_purls_for_record(
record: &mut RepoDataRecord,
custom_mapping: &HashMap<String, HashMap<String, Option<String>>>,
custom_mapping: &HashMap<String, CompressedMapping>,
) -> miette::Result<()> {
// If the package already has a pypi name we can stop here.
if record
Expand Down Expand Up @@ -151,7 +166,7 @@ fn amend_pypi_purls_for_record(

pub fn _amend_only_custom_pypi_purls(
conda_packages: &mut [RepoDataRecord],
custom_mapping: &HashMap<String, HashMap<String, Option<String>>>,
custom_mapping: &HashMap<String, CompressedMapping>,
) -> miette::Result<()> {
for record in conda_packages.iter_mut() {
amend_pypi_purls_for_record(record, custom_mapping)?;
Expand Down
46 changes: 14 additions & 32 deletions crates/pypi_mapping/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc};

use async_once_cell::OnceCell as AsyncCell;
use custom_pypi_mapping::fetch_mapping_from_path;
use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
use miette::{Context, IntoDiagnostic};
use pixi_config::get_cache_dir;
use rattler_conda_types::{PackageRecord, PackageUrl, RepoDataRecord};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use url::Url;

use crate::custom_pypi_mapping::fetch_mapping_from_url;
use pixi_config::get_cache_dir;

pub mod custom_pypi_mapping;
pub mod prefix_pypi_name_mapping;
Expand Down Expand Up @@ -62,40 +62,22 @@ impl CustomMapping {

match url {
MappingLocation::Url(url) => {
let response = client
.get(url.clone())
.send()
.await
.into_diagnostic()
.context(format!(
"failed to download pypi mapping from {} location",
url.as_str()
))?;

if !response.status().is_success() {
return Err(miette::miette!(
"Could not request mapping located at {:?}",
url.as_str()
));
}

let mapping_by_name = fetch_mapping_from_url(client, url).await?;
let mapping_by_name = match url.scheme() {
"file" => {
let file_path = url.to_file_path().map_err(|_| {
miette::miette!("{} is not a valid file url", url)
})?;
fetch_mapping_from_path(&file_path)?
}
_ => fetch_mapping_from_url(client, url).await?,
};

mapping_url_to_name.insert(name.to_string(), mapping_by_name);
}
MappingLocation::Path(path) => {
let contents = std::fs::read_to_string(path)
.into_diagnostic()
.context(format!("mapping on {path:?} could not be loaded"))?;
let data: HashMap<String, Option<String>> =
serde_json::from_str(&contents).into_diagnostic().context(
format!(
"Failed to parse JSON mapping located at {}",
path.display()
),
)?;

mapping_url_to_name.insert(name.to_string(), data);
let mapping_by_name = fetch_mapping_from_path(path)?;

mapping_url_to_name.insert(name.to_string(), mapping_by_name);
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions tests/solve_group_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,76 @@ async fn test_path_channel() {
PurlSource::ProjectDefinedMapping.as_str()
);
}

#[tokio::test]
async fn test_file_url_as_mapping_location() {
let tmp_dir = tempfile::tempdir().unwrap();
let mapping_file = tmp_dir.path().join("custom_mapping.json");

let _ = std::fs::write(
&mapping_file,
r#"
{
"pixi-something-new": "pixi-something-old"
}
"#,
);

let mapping_file_path_as_url = Url::from_file_path(
mapping_file, // .canonicalize()
// .expect("should be canonicalized"),
)
.unwrap();

let pixi = PixiControl::from_manifest(
format!(
r#"
[project]
name = "test-channel-change"
channels = ["conda-forge"]
platforms = ["linux-64"]
conda-pypi-map = {{"conda-forge" = "{}"}}
"#,
mapping_file_path_as_url.as_str()
)
.as_str(),
)
.unwrap();

let project = pixi.project().unwrap();

let client = project.authenticated_client();

let foo_bar_package = Package::build("pixi-something-new", "2").finish();

let repo_data_record = RepoDataRecord {
package_record: foo_bar_package.package_record,
file_name: "pixi-something-new".to_owned(),
url: Url::parse("https://pypi.org/simple/pixi-something-new-new/").unwrap(),
channel: "https://conda.anaconda.org/conda-forge/".to_owned(),
};

let mut packages = vec![repo_data_record];

let mapping_source = project.pypi_name_mapping_source().unwrap();

let mapping_map = mapping_source.custom().unwrap();
pypi_mapping::custom_pypi_mapping::amend_pypi_purls(client, &mapping_map, &mut packages, None)
.await
.unwrap();

let package = packages.pop().unwrap();

assert_eq!(
package
.package_record
.purls
.as_ref()
.and_then(BTreeSet::first)
.unwrap()
.qualifiers()
.get("source")
.unwrap(),
PurlSource::ProjectDefinedMapping.as_str()
);
}

0 comments on commit 6fbb610

Please sign in to comment.