use std::{
fmt::{self, Debug, Display, Formatter},
path::Path,
};
use pep508_rs::{Pep508Url, Requirement, VerbatimUrl, VerbatimUrlError, VersionOrUrl};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub(super) struct GivenVerbatimUrl(VerbatimUrl);
impl Display for GivenVerbatimUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match (self.0.scheme(), self.0.given()) {
("file", Some(given)) => f.write_str(given),
_ => Display::fmt(&self.0, f),
}
}
}
impl Pep508Url for GivenVerbatimUrl {
type Err = VerbatimUrlError;
fn parse_url(url: &str, working_dir: Option<&Path>) -> Result<Self, Self::Err> {
<VerbatimUrl as Pep508Url>::parse_url(url, working_dir).map(Self)
}
}
impl GivenVerbatimUrl {
pub(super) fn wrap_requirement(req: &Requirement<VerbatimUrl>) -> Requirement<Self> {
let Requirement {
name,
extras,
version_or_url,
marker,
origin,
} = req;
Requirement {
name: name.clone(),
extras: extras.clone(),
version_or_url: version_or_url.as_ref().map(|v| match v {
VersionOrUrl::VersionSpecifier(s) => VersionOrUrl::VersionSpecifier(s.clone()),
VersionOrUrl::Url(u) => VersionOrUrl::Url(Self(u.clone())),
}),
marker: marker.clone(),
origin: origin.clone(),
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use pep508_rs::{Requirement, VerbatimUrl};
use super::GivenVerbatimUrl;
fn base_dir() -> PathBuf {
if cfg!(windows) {
PathBuf::from("C:\\base")
} else {
PathBuf::from("/base")
}
}
fn fmt(input: &str) -> String {
let req = Requirement::<VerbatimUrl>::parse(input, base_dir()).unwrap();
GivenVerbatimUrl::wrap_requirement(&req).to_string()
}
#[test]
fn relative_file_path_is_preserved() {
let out = fmt("foo @ ./my-pkg");
assert!(out.contains("./my-pkg"), "got: {out}");
assert!(!out.contains("/base/"), "got: {out}");
assert_eq!(out, "foo @ ./my-pkg");
}
#[test]
fn parent_relative_file_path_is_preserved() {
let out = fmt("foo @ ../sibling");
assert_eq!(out, "foo @ ../sibling");
}
#[test]
fn explicit_file_scheme_url_passes_through() {
let input = if cfg!(windows) {
"foo @ file:///C:/abs/pkg"
} else {
"foo @ file:///abs/pkg"
};
let out = fmt(input);
assert_eq!(out, input);
}
#[test]
fn https_url_uses_normalized_url() {
let out = fmt("foo @ https://example.com/pkg-1.0.whl");
assert_eq!(out, "foo @ https://example.com/pkg-1.0.whl");
}
#[test]
fn version_specifier_unchanged() {
let out = fmt("foo>=1.0,<2.0");
assert_eq!(out, "foo>=1.0,<2.0");
}
#[test]
fn bare_name_unchanged() {
let out = fmt("foo");
assert_eq!(out, "foo");
}
#[test]
fn extras_and_marker_are_preserved_with_relative_path() {
let out = fmt("foo[bar,baz] @ ./pkg ; python_version > \"3.8\"");
assert!(out.starts_with("foo[bar,baz] @ ./pkg ; "), "got: {out}");
assert!(out.contains("python_full_version"), "got: {out}");
}
#[test]
fn marker_only_unchanged() {
let out = fmt("foo ; python_version > \"3.8\"");
assert!(out.starts_with("foo ; "), "got: {out}");
assert!(out.contains("python_full_version"), "got: {out}");
}
#[test]
fn absolute_file_url_without_given_uses_url_display() {
let path = if cfg!(windows) {
PathBuf::from("C:\\abs\\pkg")
} else {
PathBuf::from("/abs/pkg")
};
let url = VerbatimUrl::from_absolute_path(&path).unwrap();
assert!(url.given().is_none());
let req = Requirement::<VerbatimUrl> {
name: "foo".parse().unwrap(),
extras: vec![],
version_or_url: Some(pep508_rs::VersionOrUrl::Url(url.clone())),
marker: pep508_rs::MarkerTree::TRUE,
origin: None,
};
let out = GivenVerbatimUrl::wrap_requirement(&req).to_string();
assert!(out.starts_with("foo @ file://"), "got: {out}");
}
#[test]
fn https_url_with_stale_given_emits_actual_url() {
let url = VerbatimUrl::parse_url("https://example.com/canonical")
.unwrap()
.with_given("https://example.com/stale".to_string());
assert_eq!(url.given(), Some("https://example.com/stale"));
let req = Requirement::<VerbatimUrl> {
name: "foo".parse().unwrap(),
extras: vec![],
version_or_url: Some(pep508_rs::VersionOrUrl::Url(url)),
marker: pep508_rs::MarkerTree::TRUE,
origin: None,
};
let out = GivenVerbatimUrl::wrap_requirement(&req).to_string();
assert_eq!(out, "foo @ https://example.com/canonical");
let reparsed = Requirement::<VerbatimUrl>::parse(&out, base_dir()).unwrap();
let out2 = GivenVerbatimUrl::wrap_requirement(&reparsed).to_string();
assert_eq!(out, out2);
}
}