use std::path::PathBuf;
use std::str::FromStr;
use crate::hook::InstallInfo;
use crate::languages::version::{Error, try_into_u64_slice};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PythonRequest {
Any,
Major(u64),
MajorMinor(u64, u64),
MajorMinorPatch(u64, u64, u64),
Path(PathBuf),
Range(semver::VersionReq, String),
}
impl FromStr for PythonRequest {
type Err = Error;
fn from_str(request: &str) -> Result<Self, Self::Err> {
if request.is_empty() {
return Ok(Self::Any);
}
if let Some(version_part) = request.strip_prefix("python") {
if version_part.is_empty() {
return Ok(Self::Any);
}
Self::parse_version_numbers(version_part, request)
} else {
Self::parse_version_numbers(request, request)
.or_else(|_| {
semver::VersionReq::parse(request)
.map(|version_req| PythonRequest::Range(version_req, request.into()))
.map_err(|_| Error::InvalidVersion(request.to_string()))
})
.or_else(|_| {
let path = PathBuf::from(request);
if path.exists() {
Ok(PythonRequest::Path(path))
} else {
Err(Error::InvalidVersion(request.to_string()))
}
})
}
}
}
impl PythonRequest {
pub(crate) fn is_any(&self) -> bool {
matches!(self, PythonRequest::Any)
}
fn parse_version_numbers(
version_str: &str,
original_request: &str,
) -> Result<PythonRequest, Error> {
let parts = try_into_u64_slice(version_str)
.map_err(|_| Error::InvalidVersion(original_request.to_string()))?;
let parts = split_wheel_tag_version(parts);
match parts[..] {
[major] => Ok(PythonRequest::Major(major)),
[major, minor] => Ok(PythonRequest::MajorMinor(major, minor)),
[major, minor, patch] => Ok(PythonRequest::MajorMinorPatch(major, minor, patch)),
_ => Err(Error::InvalidVersion(original_request.to_string())),
}
}
pub(crate) fn satisfied_by(&self, install_info: &InstallInfo) -> bool {
let version = &install_info.language_version;
match self {
PythonRequest::Any => true,
PythonRequest::Major(major) => version.major == *major,
PythonRequest::MajorMinor(major, minor) => {
version.major == *major && version.minor == *minor
}
PythonRequest::MajorMinorPatch(major, minor, patch) => {
version.major == *major && version.minor == *minor && version.patch == *patch
}
PythonRequest::Path(path) => path == &install_info.toolchain,
PythonRequest::Range(req, _) => req.matches(version),
}
}
}
fn split_wheel_tag_version(mut version: Vec<u64>) -> Vec<u64> {
if version.len() != 1 {
return version;
}
let release = version[0].to_string();
let mut chars = release.chars();
let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
return version;
};
let Ok(minor) = chars.as_str().parse::<u32>() else {
return version;
};
version[0] = u64::from(major);
version.push(u64::from(minor));
version
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Language;
use rustc_hash::FxHashSet;
#[test]
fn test_parse_python_request() {
assert_eq!(PythonRequest::from_str("").unwrap(), PythonRequest::Any);
assert_eq!(
PythonRequest::from_str("python").unwrap(),
PythonRequest::Any
);
assert_eq!(
PythonRequest::from_str("python3").unwrap(),
PythonRequest::Major(3)
);
assert_eq!(
PythonRequest::from_str("python3.12").unwrap(),
PythonRequest::MajorMinor(3, 12)
);
assert_eq!(
PythonRequest::from_str("python3.13.2").unwrap(),
PythonRequest::MajorMinorPatch(3, 13, 2)
);
assert_eq!(
PythonRequest::from_str("3").unwrap(),
PythonRequest::Major(3)
);
assert_eq!(
PythonRequest::from_str("3.12").unwrap(),
PythonRequest::MajorMinor(3, 12)
);
assert_eq!(
PythonRequest::from_str("3.12.3").unwrap(),
PythonRequest::MajorMinorPatch(3, 12, 3)
);
assert_eq!(
PythonRequest::from_str("312").unwrap(),
PythonRequest::MajorMinor(3, 12)
);
assert_eq!(
PythonRequest::from_str("python312").unwrap(),
PythonRequest::MajorMinor(3, 12)
);
assert_eq!(
PythonRequest::from_str(">=3.12").unwrap(),
PythonRequest::Range(
semver::VersionReq::parse(">=3.12").unwrap(),
">=3.12".to_string()
)
);
assert_eq!(
PythonRequest::from_str(">=3.8, <3.12").unwrap(),
PythonRequest::Range(
semver::VersionReq::parse(">=3.8, <3.12").unwrap(),
">=3.8, <3.12".to_string()
)
);
assert!(PythonRequest::from_str("invalid").is_err());
assert!(PythonRequest::from_str("3.12.3.4").is_err());
assert!(PythonRequest::from_str("3.12.a").is_err());
assert!(PythonRequest::from_str("3.b.1").is_err());
assert!(PythonRequest::from_str("3..2").is_err());
assert!(PythonRequest::from_str("a3.12").is_err());
assert!(PythonRequest::from_str("3.12.3a1").is_err());
assert!(PythonRequest::from_str("3.12.3rc1").is_err());
assert!(PythonRequest::from_str("python3.13.2a1").is_err());
assert!(PythonRequest::from_str("python3.13.2rc1").is_err());
assert!(PythonRequest::from_str("python3.13.2t1").is_err());
assert!(PythonRequest::from_str("python3.13.2-64").is_err());
assert!(PythonRequest::from_str("python3.13.2-64").is_err());
}
#[test]
fn test_satisfied_by() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let mut install_info =
InstallInfo::new(Language::Python, FxHashSet::default(), temp_dir.path())?;
install_info
.with_language_version(semver::Version::new(3, 12, 1))
.with_toolchain(PathBuf::from("/usr/bin/python3.12"));
assert!(PythonRequest::Any.satisfied_by(&install_info));
assert!(PythonRequest::Major(3).satisfied_by(&install_info));
assert!(PythonRequest::MajorMinor(3, 12).satisfied_by(&install_info));
assert!(PythonRequest::MajorMinorPatch(3, 12, 1).satisfied_by(&install_info));
assert!(!PythonRequest::MajorMinorPatch(3, 12, 2).satisfied_by(&install_info));
assert!(
PythonRequest::Path(PathBuf::from("/usr/bin/python3.12")).satisfied_by(&install_info)
);
assert!(
!PythonRequest::Path(PathBuf::from("/usr/bin/python3.11")).satisfied_by(&install_info)
);
let range_req = semver::VersionReq::parse(">=3.12").unwrap();
assert!(
PythonRequest::Range(range_req.clone(), ">=3.12".to_string())
.satisfied_by(&install_info)
);
let range_req = semver::VersionReq::parse(">=4.0").unwrap();
assert!(!PythonRequest::Range(range_req, ">=4.0".to_string()).satisfied_by(&install_info));
Ok(())
}
}