mirror of https://github.com/astral-sh/uv
Use structured types for parsing and formatting language and ABI tags (#10525)
## Summary I need to be able to do non-lexicographic comparisons between tags (e.g., so I can sort `cp313` as greater than `cp39`). It ended up being easiest to just create structured types for all the tags we support, with `FromStr` and `Display` implementations. We don't currently store these in `Tags` or in `WheelFilename`. We may want to, since they're really small (and `Copy`), but I need to benchmark to determine whether parsing these in `WheelFilename` is prohibitively slow.
This commit is contained in:
parent
73f60bbd2c
commit
e0e8ba582a
|
|
@ -177,6 +177,8 @@ impl WheelFilename {
|
|||
name,
|
||||
version,
|
||||
build_tag,
|
||||
// TODO(charlie): Consider storing structured tags here. We need to benchmark to
|
||||
// understand whether it's impactful.
|
||||
python_tag: python_tag.split('.').map(String::from).collect(),
|
||||
abi_tag: abi_tag.split('.').map(String::from).collect(),
|
||||
platform_tag: platform_tag.split('.').map(String::from).collect(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,446 @@
|
|||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A tag to represent the ABI compatibility of a Python distribution.
|
||||
///
|
||||
/// This is the second segment in the wheel filename, following the language tag. For example,
|
||||
/// in `cp39-none-manylinux_2_24_x86_64.whl`, the ABI tag is `none`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum AbiTag {
|
||||
/// Ex) `none`
|
||||
None,
|
||||
/// Ex) `abi3`
|
||||
Abi3,
|
||||
/// Ex) `cp39m`, `cp310t`
|
||||
CPython {
|
||||
gil_disabled: bool,
|
||||
python_version: (u8, u8),
|
||||
},
|
||||
/// Ex) `pypy39_pp73`
|
||||
PyPy {
|
||||
python_version: (u8, u8),
|
||||
implementation_version: (u8, u8),
|
||||
},
|
||||
/// Ex) `graalpy310_graalpy240_310_native`
|
||||
GraalPy {
|
||||
python_version: (u8, u8),
|
||||
implementation_version: (u8, u8),
|
||||
},
|
||||
/// Ex) `pyston38-pyston_23`
|
||||
Pyston {
|
||||
python_version: (u8, u8),
|
||||
implementation_version: (u8, u8),
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AbiTag {
|
||||
/// Format an [`AbiTag`] as a string.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "none"),
|
||||
Self::Abi3 => write!(f, "abi3"),
|
||||
Self::CPython {
|
||||
gil_disabled,
|
||||
python_version: (major, minor),
|
||||
} => {
|
||||
if *minor <= 7 {
|
||||
write!(f, "cp{major}{minor}m")
|
||||
} else if *gil_disabled {
|
||||
// https://peps.python.org/pep-0703/#build-configuration-changes
|
||||
// Python 3.13+ only, but it makes more sense to just rely on the sysconfig var.
|
||||
write!(f, "cp{major}{minor}t")
|
||||
} else {
|
||||
write!(f, "cp{major}{minor}")
|
||||
}
|
||||
}
|
||||
Self::PyPy {
|
||||
python_version: (py_major, py_minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
} => {
|
||||
write!(f, "pypy{py_major}{py_minor}_pp{impl_major}{impl_minor}")
|
||||
}
|
||||
Self::GraalPy {
|
||||
python_version: (py_major, py_minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"graalpy{py_major}{py_minor}_graalpy{impl_major}{impl_minor}_{py_major}{py_minor}native"
|
||||
)
|
||||
}
|
||||
Self::Pyston {
|
||||
python_version: (py_major, py_minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"pyston{py_major}{py_minor}-pyston_{impl_major}{impl_minor}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AbiTag {
|
||||
type Err = ParseAbiTagError;
|
||||
|
||||
/// Parse an [`AbiTag`] from a string.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
/// Parse a Python version from a string (e.g., convert `39` into `(3, 9)`).
|
||||
fn parse_python_version(
|
||||
version_str: &str,
|
||||
implementation: &'static str,
|
||||
full_tag: &str,
|
||||
) -> Result<(u8, u8), ParseAbiTagError> {
|
||||
let major = version_str
|
||||
.as_bytes()
|
||||
.first()
|
||||
.ok_or_else(|| ParseAbiTagError::MissingMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.checked_sub(b'0')
|
||||
.and_then(|d| if d < 10 { Some(d) } else { None })
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?;
|
||||
let minor = version_str
|
||||
.get(1..)
|
||||
.ok_or_else(|| ParseAbiTagError::MissingMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.parse::<u8>()
|
||||
.map_err(|_| ParseAbiTagError::InvalidMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?;
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
/// Parse an implementation version from a string (e.g., convert `37` into `(3, 7)`).
|
||||
fn parse_impl_version(
|
||||
version_str: &str,
|
||||
implementation: &'static str,
|
||||
full_tag: &str,
|
||||
) -> Result<(u8, u8), ParseAbiTagError> {
|
||||
let major = version_str
|
||||
.as_bytes()
|
||||
.first()
|
||||
.ok_or_else(|| ParseAbiTagError::MissingImplMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.checked_sub(b'0')
|
||||
.and_then(|d| if d < 10 { Some(d) } else { None })
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidImplMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?;
|
||||
let minor = version_str
|
||||
.get(1..)
|
||||
.ok_or_else(|| ParseAbiTagError::MissingImplMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.parse::<u8>()
|
||||
.map_err(|_| ParseAbiTagError::InvalidImplMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?;
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
if s == "none" {
|
||||
Ok(Self::None)
|
||||
} else if s == "abi3" {
|
||||
Ok(Self::Abi3)
|
||||
} else if let Some(cp) = s.strip_prefix("cp") {
|
||||
// Ex) `cp39m`, `cp310t`
|
||||
let version_end = cp.find(|c: char| !c.is_ascii_digit()).unwrap_or(cp.len());
|
||||
let version_str = &cp[..version_end];
|
||||
let (major, minor) = parse_python_version(version_str, "CPython", s)?;
|
||||
let gil_disabled = cp.ends_with('t');
|
||||
Ok(Self::CPython {
|
||||
gil_disabled,
|
||||
python_version: (major, minor),
|
||||
})
|
||||
} else if let Some(rest) = s.strip_prefix("pypy") {
|
||||
// Ex) `pypy39_pp73`
|
||||
let (version_str, rest) =
|
||||
rest.split_once('_')
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "PyPy",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let (major, minor) = parse_python_version(version_str, "PyPy", s)?;
|
||||
let rest = rest
|
||||
.strip_prefix("pp")
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "PyPy",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let (impl_major, impl_minor) = parse_impl_version(rest, "PyPy", s)?;
|
||||
Ok(Self::PyPy {
|
||||
python_version: (major, minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
})
|
||||
} else if let Some(rest) = s.strip_prefix("graalpy") {
|
||||
// Ex) `graalpy310_graalpy240_310_native`
|
||||
let version_end = rest
|
||||
.find('_')
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "GraalPy",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let version_str = &rest[..version_end];
|
||||
let (major, minor) = parse_python_version(version_str, "GraalPy", s)?;
|
||||
let rest = rest[version_end + 1..]
|
||||
.strip_prefix("graalpy")
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "GraalPy",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let (impl_major, impl_minor) = parse_impl_version(rest, "GraalPy", s)?;
|
||||
Ok(Self::GraalPy {
|
||||
python_version: (major, minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
})
|
||||
} else if let Some(rest) = s.strip_prefix("pyston") {
|
||||
// Ex) `pyston38-pyston_23`
|
||||
let version_end = rest
|
||||
.find('-')
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "Pyston",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let version_str = &rest[..version_end];
|
||||
let (major, minor) = parse_python_version(version_str, "Pyston", s)?;
|
||||
let rest = rest[version_end + 1..]
|
||||
.strip_prefix("pyston_")
|
||||
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
|
||||
implementation: "Pyston",
|
||||
tag: s.to_string(),
|
||||
})?;
|
||||
let (impl_major, impl_minor) = parse_impl_version(rest, "Pyston", s)?;
|
||||
Ok(Self::Pyston {
|
||||
python_version: (major, minor),
|
||||
implementation_version: (impl_major, impl_minor),
|
||||
})
|
||||
} else {
|
||||
Err(ParseAbiTagError::UnknownFormat(s.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum ParseAbiTagError {
|
||||
#[error("Unknown ABI tag format: {0}")]
|
||||
UnknownFormat(String),
|
||||
#[error("Missing major version in {implementation} ABI tag: {tag}")]
|
||||
MissingMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid major version in {implementation} ABI tag: {tag}")]
|
||||
InvalidMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Missing minor version in {implementation} ABI tag: {tag}")]
|
||||
MissingMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid minor version in {implementation} ABI tag: {tag}")]
|
||||
InvalidMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid {implementation} ABI tag format: {tag}")]
|
||||
InvalidFormat {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Missing implementation major version in {implementation} ABI tag: {tag}")]
|
||||
MissingImplMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid implementation major version in {implementation} ABI tag: {tag}")]
|
||||
InvalidImplMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Missing implementation minor version in {implementation} ABI tag: {tag}")]
|
||||
MissingImplMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid implementation minor version in {implementation} ABI tag: {tag}")]
|
||||
InvalidImplMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::abi_tag::{AbiTag, ParseAbiTagError};
|
||||
|
||||
#[test]
|
||||
fn none_abi() {
|
||||
assert_eq!(AbiTag::from_str("none"), Ok(AbiTag::None));
|
||||
assert_eq!(AbiTag::None.to_string(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi3() {
|
||||
assert_eq!(AbiTag::from_str("abi3"), Ok(AbiTag::Abi3));
|
||||
assert_eq!(AbiTag::Abi3.to_string(), "abi3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython_abi() {
|
||||
let tag = AbiTag::CPython {
|
||||
gil_disabled: false,
|
||||
python_version: (3, 9),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("cp39"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "cp39");
|
||||
|
||||
let tag = AbiTag::CPython {
|
||||
gil_disabled: false,
|
||||
python_version: (3, 7),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("cp37m"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "cp37m");
|
||||
|
||||
let tag = AbiTag::CPython {
|
||||
gil_disabled: true,
|
||||
python_version: (3, 13),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("cp313t"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "cp313t");
|
||||
|
||||
assert_eq!(
|
||||
AbiTag::from_str("cpXY"),
|
||||
Err(ParseAbiTagError::MissingMajorVersion {
|
||||
implementation: "CPython",
|
||||
tag: "cpXY".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pypy_abi() {
|
||||
let tag = AbiTag::PyPy {
|
||||
python_version: (3, 9),
|
||||
implementation_version: (7, 3),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("pypy39_pp73"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "pypy39_pp73");
|
||||
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pypy39"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "PyPy",
|
||||
tag: "pypy39".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pypy39_73"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "PyPy",
|
||||
tag: "pypy39_73".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pypy39_ppXY"),
|
||||
Err(ParseAbiTagError::InvalidImplMajorVersion {
|
||||
implementation: "PyPy",
|
||||
tag: "pypy39_ppXY".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graalpy_abi() {
|
||||
let tag = AbiTag::GraalPy {
|
||||
python_version: (3, 10),
|
||||
implementation_version: (2, 40),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("graalpy310_graalpy240"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "graalpy310_graalpy240_310native");
|
||||
|
||||
assert_eq!(
|
||||
AbiTag::from_str("graalpy310"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpy310".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("graalpy310_240"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpy310_240".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("graalpy310_graalpyXY"),
|
||||
Err(ParseAbiTagError::InvalidImplMajorVersion {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpy310_graalpyXY".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyston_abi() {
|
||||
let tag = AbiTag::Pyston {
|
||||
python_version: (3, 8),
|
||||
implementation_version: (2, 3),
|
||||
};
|
||||
assert_eq!(AbiTag::from_str("pyston38-pyston_23"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "pyston38-pyston_23");
|
||||
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pyston38"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "Pyston",
|
||||
tag: "pyston38".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pyston38_23"),
|
||||
Err(ParseAbiTagError::InvalidFormat {
|
||||
implementation: "Pyston",
|
||||
tag: "pyston38_23".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str("pyston38-pyston_XY"),
|
||||
Err(ParseAbiTagError::InvalidImplMajorVersion {
|
||||
implementation: "Pyston",
|
||||
tag: "pyston38-pyston_XY".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_abi() {
|
||||
assert_eq!(
|
||||
AbiTag::from_str("unknown"),
|
||||
Err(ParseAbiTagError::UnknownFormat("unknown".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
AbiTag::from_str(""),
|
||||
Err(ParseAbiTagError::UnknownFormat(String::new()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A tag to represent the language and implementation of the Python interpreter.
|
||||
///
|
||||
/// This is the first segment in the wheel filename. For example, in `cp39-none-manylinux_2_24_x86_64.whl`,
|
||||
/// the language tag is `cp39`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum LanguageTag {
|
||||
/// Ex) `none`
|
||||
None,
|
||||
/// Ex) `py3`, `py39`
|
||||
Python { major: u8, minor: Option<u8> },
|
||||
/// Ex) `cp39`
|
||||
CPython { python_version: (u8, u8) },
|
||||
/// Ex) `pp39`
|
||||
PyPy { python_version: (u8, u8) },
|
||||
/// Ex) `graalpy310`
|
||||
GraalPy { python_version: (u8, u8) },
|
||||
/// Ex) `pt38`
|
||||
Pyston { python_version: (u8, u8) },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LanguageTag {
|
||||
/// Format a [`LanguageTag`] as a string.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "none"),
|
||||
Self::Python { major, minor } => {
|
||||
if let Some(minor) = minor {
|
||||
write!(f, "py{major}{minor}")
|
||||
} else {
|
||||
write!(f, "py{major}")
|
||||
}
|
||||
}
|
||||
Self::CPython {
|
||||
python_version: (major, minor),
|
||||
} => {
|
||||
write!(f, "cp{major}{minor}")
|
||||
}
|
||||
Self::PyPy {
|
||||
python_version: (major, minor),
|
||||
} => {
|
||||
write!(f, "pp{major}{minor}")
|
||||
}
|
||||
Self::GraalPy {
|
||||
python_version: (major, minor),
|
||||
} => {
|
||||
write!(f, "graalpy{major}{minor}")
|
||||
}
|
||||
Self::Pyston {
|
||||
python_version: (major, minor),
|
||||
} => {
|
||||
write!(f, "pt{major}{minor}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LanguageTag {
|
||||
type Err = ParseLanguageTagError;
|
||||
|
||||
/// Parse a [`LanguageTag`] from a string.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
/// Parse a Python version from a string (e.g., convert `39` into `(3, 9)`).
|
||||
fn parse_python_version(
|
||||
version_str: &str,
|
||||
implementation: &'static str,
|
||||
full_tag: &str,
|
||||
) -> Result<(u8, u8), ParseLanguageTagError> {
|
||||
let major = version_str
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.to_digit(10)
|
||||
.ok_or_else(|| ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})? as u8;
|
||||
let minor = version_str
|
||||
.get(1..)
|
||||
.ok_or_else(|| ParseLanguageTagError::MissingMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?
|
||||
.parse::<u8>()
|
||||
.map_err(|_| ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation,
|
||||
tag: full_tag.to_string(),
|
||||
})?;
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
if s == "none" {
|
||||
Ok(Self::None)
|
||||
} else if let Some(py) = s.strip_prefix("py") {
|
||||
if py.len() == 1 {
|
||||
// Ex) `py3`
|
||||
let major = py
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "Python",
|
||||
tag: s.to_string(),
|
||||
})?
|
||||
.to_digit(10)
|
||||
.ok_or_else(|| ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "Python",
|
||||
tag: s.to_string(),
|
||||
})? as u8;
|
||||
Ok(Self::Python { major, minor: None })
|
||||
} else {
|
||||
// Ex) `py39`
|
||||
let (major, minor) = parse_python_version(py, "Python", s)?;
|
||||
Ok(Self::Python {
|
||||
major,
|
||||
minor: Some(minor),
|
||||
})
|
||||
}
|
||||
} else if let Some(cp) = s.strip_prefix("cp") {
|
||||
// Ex) `cp39`
|
||||
let (major, minor) = parse_python_version(cp, "CPython", s)?;
|
||||
Ok(Self::CPython {
|
||||
python_version: (major, minor),
|
||||
})
|
||||
} else if let Some(pp) = s.strip_prefix("pp") {
|
||||
// Ex) `pp39`
|
||||
let (major, minor) = parse_python_version(pp, "PyPy", s)?;
|
||||
Ok(Self::PyPy {
|
||||
python_version: (major, minor),
|
||||
})
|
||||
} else if let Some(graalpy) = s.strip_prefix("graalpy") {
|
||||
// Ex) `graalpy310`
|
||||
let (major, minor) = parse_python_version(graalpy, "GraalPy", s)?;
|
||||
Ok(Self::GraalPy {
|
||||
python_version: (major, minor),
|
||||
})
|
||||
} else if let Some(pt) = s.strip_prefix("pt") {
|
||||
// Ex) `pt38`
|
||||
let (major, minor) = parse_python_version(pt, "Pyston", s)?;
|
||||
Ok(Self::Pyston {
|
||||
python_version: (major, minor),
|
||||
})
|
||||
} else {
|
||||
Err(ParseLanguageTagError::UnknownFormat(s.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum ParseLanguageTagError {
|
||||
#[error("Unknown language tag format: {0}")]
|
||||
UnknownFormat(String),
|
||||
#[error("Missing major version in {implementation} language tag: {tag}")]
|
||||
MissingMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid major version in {implementation} language tag: {tag}")]
|
||||
InvalidMajorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Missing minor version in {implementation} language tag: {tag}")]
|
||||
MissingMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
#[error("Invalid minor version in {implementation} language tag: {tag}")]
|
||||
InvalidMinorVersion {
|
||||
implementation: &'static str,
|
||||
tag: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::language_tag::ParseLanguageTagError;
|
||||
use crate::LanguageTag;
|
||||
|
||||
#[test]
|
||||
fn none() {
|
||||
assert_eq!(LanguageTag::from_str("none"), Ok(LanguageTag::None));
|
||||
assert_eq!(LanguageTag::None.to_string(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_language() {
|
||||
let tag = LanguageTag::Python {
|
||||
major: 3,
|
||||
minor: None,
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("py3"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "py3");
|
||||
|
||||
let tag = LanguageTag::Python {
|
||||
major: 3,
|
||||
minor: Some(9),
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("py39"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "py39");
|
||||
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("py"),
|
||||
Err(ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "Python",
|
||||
tag: "py".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("pyX"),
|
||||
Err(ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "Python",
|
||||
tag: "pyX".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("py3X"),
|
||||
Err(ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation: "Python",
|
||||
tag: "py3X".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpython_language() {
|
||||
let tag = LanguageTag::CPython {
|
||||
python_version: (3, 9),
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("cp39"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "cp39");
|
||||
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("cp"),
|
||||
Err(ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "CPython",
|
||||
tag: "cp".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("cpX"),
|
||||
Err(ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "CPython",
|
||||
tag: "cpX".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("cp3X"),
|
||||
Err(ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation: "CPython",
|
||||
tag: "cp3X".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pypy_language() {
|
||||
let tag = LanguageTag::PyPy {
|
||||
python_version: (3, 9),
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("pp39"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "pp39");
|
||||
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("pp"),
|
||||
Err(ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "PyPy",
|
||||
tag: "pp".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("ppX"),
|
||||
Err(ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "PyPy",
|
||||
tag: "ppX".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("pp3X"),
|
||||
Err(ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation: "PyPy",
|
||||
tag: "pp3X".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graalpy_language() {
|
||||
let tag = LanguageTag::GraalPy {
|
||||
python_version: (3, 10),
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("graalpy310"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "graalpy310");
|
||||
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("graalpy"),
|
||||
Err(ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpy".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("graalpyX"),
|
||||
Err(ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpyX".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("graalpy3X"),
|
||||
Err(ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation: "GraalPy",
|
||||
tag: "graalpy3X".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyston_language() {
|
||||
let tag = LanguageTag::Pyston {
|
||||
python_version: (3, 8),
|
||||
};
|
||||
assert_eq!(LanguageTag::from_str("pt38"), Ok(tag));
|
||||
assert_eq!(tag.to_string(), "pt38");
|
||||
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("pt"),
|
||||
Err(ParseLanguageTagError::MissingMajorVersion {
|
||||
implementation: "Pyston",
|
||||
tag: "pt".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("ptX"),
|
||||
Err(ParseLanguageTagError::InvalidMajorVersion {
|
||||
implementation: "Pyston",
|
||||
tag: "ptX".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("pt3X"),
|
||||
Err(ParseLanguageTagError::InvalidMinorVersion {
|
||||
implementation: "Pyston",
|
||||
tag: "pt3X".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_language() {
|
||||
assert_eq!(
|
||||
LanguageTag::from_str("unknown"),
|
||||
Err(ParseLanguageTagError::UnknownFormat("unknown".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
LanguageTag::from_str(""),
|
||||
Err(ParseLanguageTagError::UnknownFormat(String::new()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
pub use abi_tag::AbiTag;
|
||||
pub use language_tag::LanguageTag;
|
||||
pub use platform::{Arch, Os, Platform, PlatformError};
|
||||
pub use tags::{IncompatibleTag, TagCompatibility, TagPriority, Tags, TagsError};
|
||||
|
||||
mod abi_tag;
|
||||
mod language_tag;
|
||||
mod platform;
|
||||
mod tags;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ use std::{cmp, num::NonZeroU32};
|
|||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{Arch, Os, Platform, PlatformError};
|
||||
use crate::abi_tag::AbiTag;
|
||||
use crate::{Arch, LanguageTag, Os, Platform, PlatformError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TagsError {
|
||||
|
|
@ -82,6 +83,7 @@ impl Tags {
|
|||
/// Tags are prioritized based on their position in the given vector. Specifically, tags that
|
||||
/// appear earlier in the vector are given higher priority than tags that appear later.
|
||||
pub fn new(tags: Vec<(String, String, String)>) -> Self {
|
||||
// Index the tags by Python version, ABI, and platform.
|
||||
let mut map = FxHashMap::default();
|
||||
for (index, (py, abi, platform)) in tags.into_iter().rev().enumerate() {
|
||||
map.entry(py)
|
||||
|
|
@ -120,8 +122,10 @@ impl Tags {
|
|||
// 1. This exact c api version
|
||||
for platform_tag in &platform_tags {
|
||||
tags.push((
|
||||
implementation.language_tag(python_version),
|
||||
implementation.abi_tag(python_version, implementation_version),
|
||||
implementation.language_tag(python_version).to_string(),
|
||||
implementation
|
||||
.abi_tag(python_version, implementation_version)
|
||||
.to_string(),
|
||||
platform_tag.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -133,8 +137,10 @@ impl Tags {
|
|||
if !gil_disabled {
|
||||
for platform_tag in &platform_tags {
|
||||
tags.push((
|
||||
implementation.language_tag((python_version.0, minor)),
|
||||
"abi3".to_string(),
|
||||
implementation
|
||||
.language_tag((python_version.0, minor))
|
||||
.to_string(),
|
||||
AbiTag::Abi3.to_string(),
|
||||
platform_tag.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -143,8 +149,10 @@ impl Tags {
|
|||
if minor == python_version.1 {
|
||||
for platform_tag in &platform_tags {
|
||||
tags.push((
|
||||
implementation.language_tag((python_version.0, minor)),
|
||||
"none".to_string(),
|
||||
implementation
|
||||
.language_tag((python_version.0, minor))
|
||||
.to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
platform_tag.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -155,8 +163,12 @@ impl Tags {
|
|||
for minor in (0..=python_version.1).rev() {
|
||||
for platform_tag in &platform_tags {
|
||||
tags.push((
|
||||
format!("py{}{}", python_version.0, minor),
|
||||
"none".to_string(),
|
||||
LanguageTag::Python {
|
||||
major: python_version.0,
|
||||
minor: Some(minor),
|
||||
}
|
||||
.to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
platform_tag.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -165,7 +177,7 @@ impl Tags {
|
|||
for platform_tag in &platform_tags {
|
||||
tags.push((
|
||||
format!("py{}", python_version.0),
|
||||
"none".to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
platform_tag.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -174,22 +186,30 @@ impl Tags {
|
|||
// 4. no binary
|
||||
if matches!(implementation, Implementation::CPython { .. }) {
|
||||
tags.push((
|
||||
implementation.language_tag(python_version),
|
||||
"none".to_string(),
|
||||
implementation.language_tag(python_version).to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
"any".to_string(),
|
||||
));
|
||||
}
|
||||
for minor in (0..=python_version.1).rev() {
|
||||
tags.push((
|
||||
format!("py{}{}", python_version.0, minor),
|
||||
"none".to_string(),
|
||||
LanguageTag::Python {
|
||||
major: python_version.0,
|
||||
minor: Some(minor),
|
||||
}
|
||||
.to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
"any".to_string(),
|
||||
));
|
||||
// After the matching version emit `none` tags for the major version i.e. `py3`
|
||||
if minor == python_version.1 {
|
||||
tags.push((
|
||||
format!("py{}", python_version.0),
|
||||
"none".to_string(),
|
||||
LanguageTag::Python {
|
||||
major: python_version.0,
|
||||
minor: None,
|
||||
}
|
||||
.to_string(),
|
||||
AbiTag::None.to_string(),
|
||||
"any".to_string(),
|
||||
));
|
||||
}
|
||||
|
|
@ -321,64 +341,41 @@ enum Implementation {
|
|||
impl Implementation {
|
||||
/// Returns the "language implementation and version tag" for the current implementation and
|
||||
/// Python version (e.g., `cp39` or `pp37`).
|
||||
fn language_tag(self, python_version: (u8, u8)) -> String {
|
||||
fn language_tag(self, python_version: (u8, u8)) -> LanguageTag {
|
||||
match self {
|
||||
// Ex) `cp39`
|
||||
Self::CPython { .. } => format!("cp{}{}", python_version.0, python_version.1),
|
||||
Self::CPython { .. } => LanguageTag::CPython { python_version },
|
||||
// Ex) `pp39`
|
||||
Self::PyPy => format!("pp{}{}", python_version.0, python_version.1),
|
||||
Self::PyPy => LanguageTag::PyPy { python_version },
|
||||
// Ex) `graalpy310`
|
||||
Self::GraalPy => format!("graalpy{}{}", python_version.0, python_version.1),
|
||||
Self::GraalPy => LanguageTag::GraalPy { python_version },
|
||||
// Ex) `pt38``
|
||||
Self::Pyston => format!("pt{}{}", python_version.0, python_version.1),
|
||||
Self::Pyston => LanguageTag::Pyston { python_version },
|
||||
}
|
||||
}
|
||||
|
||||
fn abi_tag(self, python_version: (u8, u8), implementation_version: (u8, u8)) -> String {
|
||||
fn abi_tag(self, python_version: (u8, u8), implementation_version: (u8, u8)) -> AbiTag {
|
||||
match self {
|
||||
// Ex) `cp39`
|
||||
Self::CPython { gil_disabled } => {
|
||||
if python_version.1 <= 7 {
|
||||
format!("cp{}{}m", python_version.0, python_version.1)
|
||||
} else if gil_disabled {
|
||||
// https://peps.python.org/pep-0703/#build-configuration-changes
|
||||
// Python 3.13+ only, but it makes more sense to just rely on the sysconfig var.
|
||||
format!("cp{}{}t", python_version.0, python_version.1)
|
||||
} else {
|
||||
format!(
|
||||
"cp{}{}{}",
|
||||
python_version.0,
|
||||
python_version.1,
|
||||
if gil_disabled { "t" } else { "" }
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::CPython { gil_disabled } => AbiTag::CPython {
|
||||
gil_disabled,
|
||||
python_version,
|
||||
},
|
||||
// Ex) `pypy39_pp73`
|
||||
Self::PyPy => format!(
|
||||
"pypy{}{}_pp{}{}",
|
||||
python_version.0,
|
||||
python_version.1,
|
||||
implementation_version.0,
|
||||
implementation_version.1
|
||||
),
|
||||
Self::PyPy => AbiTag::PyPy {
|
||||
python_version,
|
||||
implementation_version,
|
||||
},
|
||||
// Ex) `graalpy310_graalpy240_310_native
|
||||
Self::GraalPy => format!(
|
||||
"graalpy{}{}_graalpy{}{}_{}{}_native",
|
||||
python_version.0,
|
||||
python_version.1,
|
||||
implementation_version.0,
|
||||
implementation_version.1,
|
||||
python_version.0,
|
||||
python_version.1
|
||||
),
|
||||
Self::GraalPy => AbiTag::GraalPy {
|
||||
python_version,
|
||||
implementation_version,
|
||||
},
|
||||
// Ex) `pyston38-pyston_23`
|
||||
Self::Pyston => format!(
|
||||
"pyston{}{}-pyston_{}{}",
|
||||
python_version.0,
|
||||
python_version.1,
|
||||
implementation_version.0,
|
||||
implementation_version.1
|
||||
),
|
||||
Self::Pyston => AbiTag::Pyston {
|
||||
python_version,
|
||||
implementation_version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue