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:
Charlie Marsh 2025-01-13 19:49:43 -05:00 committed by GitHub
parent 73f60bbd2c
commit e0e8ba582a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 877 additions and 61 deletions

View File

@ -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(),

View File

@ -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()))
);
}
}

View File

@ -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()))
);
}
}

View File

@ -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;

View File

@ -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,
},
}
}