Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow .dist-info names with dashes for post releases #7208

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions crates/install-wheel-rs/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use zip::ZipArchive;

use distribution_filename::WheelFilename;
use pep440_rs::Version;
use uv_normalize::PackageName;
use uv_normalize::DistInfoName;

use crate::Error;

Expand Down Expand Up @@ -50,16 +50,19 @@ pub fn find_archive_dist_info<'a, T: Copy>(

// Like `pip`, validate that the `.dist-info` directory is prefixed with the canonical
// package name, but only warn if the version is not the normalized version.
let Some((name, version)) = dist_info_prefix.rsplit_once('-') else {
return Err(Error::MissingDistInfoSegments(dist_info_prefix.to_string()));
};
if PackageName::from_str(name)? != filename.name {
let normalized_prefix = DistInfoName::new(dist_info_prefix);
let Some(rest) = normalized_prefix
.as_ref()
.strip_prefix(filename.name.as_str())
else {
return Err(Error::MissingDistInfoPackageName(
dist_info_prefix.to_string(),
filename.name.to_string(),
));
}
if !Version::from_str(version).is_ok_and(|version| version == filename.version) {
};
if !rest.strip_prefix('-').is_some_and(|version| {
Version::from_str(version).is_ok_and(|version| version == filename.version)
}) {
warn!(
"{}",
Error::MissingDistInfoVersion(
Expand Down Expand Up @@ -87,16 +90,19 @@ pub fn is_metadata_entry(path: &str, filename: &WheelFilename) -> Result<bool, E

// Like `pip`, validate that the `.dist-info` directory is prefixed with the canonical
// package name, but only warn if the version is not the normalized version.
let Some((name, version)) = dist_info_prefix.rsplit_once('-') else {
return Err(Error::MissingDistInfoSegments(dist_info_prefix.to_string()));
};
if PackageName::from_str(name)? != filename.name {
let normalized_prefix = DistInfoName::new(dist_info_prefix);
let Some(rest) = normalized_prefix
.as_ref()
.strip_prefix(filename.name.as_str())
else {
return Err(Error::MissingDistInfoPackageName(
dist_info_prefix.to_string(),
filename.name.to_string(),
));
}
if !Version::from_str(version).is_ok_and(|version| version == filename.version) {
};
if !rest.strip_prefix('-').is_some_and(|version| {
Version::from_str(version).is_ok_and(|version| version == filename.version)
}) {
warn!(
"{}",
Error::MissingDistInfoVersion(
Expand Down Expand Up @@ -160,16 +166,19 @@ pub fn find_flat_dist_info(

// Like `pip`, validate that the `.dist-info` directory is prefixed with the canonical
// package name, but only warn if the version is not the normalized version.
let Some((name, version)) = dist_info_prefix.rsplit_once('-') else {
return Err(Error::MissingDistInfoSegments(dist_info_prefix.to_string()));
};
if PackageName::from_str(name)? != filename.name {
let normalized_prefix = DistInfoName::new(&dist_info_prefix);
let Some(rest) = normalized_prefix
.as_ref()
.strip_prefix(filename.name.as_str())
else {
return Err(Error::MissingDistInfoPackageName(
dist_info_prefix.to_string(),
filename.name.to_string(),
));
}
if !Version::from_str(version).is_ok_and(|version| version == filename.version) {
};
if !rest.strip_prefix('-').is_some_and(|version| {
Version::from_str(version).is_ok_and(|version| version == filename.version)
}) {
warn!(
"{}",
Error::MissingDistInfoVersion(
Expand Down
108 changes: 108 additions & 0 deletions crates/uv-normalize/src/dist_info_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::borrow::Cow;
use std::fmt;
use std::fmt::{Display, Formatter};

/// The normalized name of a `.dist-info` directory.
///
/// Like [`PackageName`](crate::PackageName), but without restrictions on the set of allowed
/// characters, etc.
///
/// See: <https://github.com/pypa/pip/blob/111eed14b6e9fba7c78a5ec2b7594812d17b5d2b/src/pip/_vendor/packaging/utils.py#L45>
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DistInfoName<'a>(Cow<'a, str>);

impl<'a> DistInfoName<'a> {
/// Create a validated, normalized extra name.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why extra name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, tragically, a typo.

pub fn new(name: &'a str) -> Self {
if Self::is_normalized(name) {
Self(Cow::Borrowed(name))
} else {
Self(Cow::Owned(Self::normalize(name)))
}
}

/// Normalize a `.dist-info` name, converting it to lowercase and collapsing runs
/// of `-`, `_`, and `.` down to a single `-`.
fn normalize(name: impl AsRef<str>) -> String {
let mut normalized = String::with_capacity(name.as_ref().len());
let mut last = None;
for char in name.as_ref().bytes() {
match char {
b'A'..=b'Z' => {
normalized.push(char.to_ascii_lowercase() as char);
}
b'-' | b'_' | b'.' => {
if matches!(last, Some(b'-' | b'_' | b'.')) {
continue;
}
normalized.push('-');
}
_ => {
normalized.push(char as char);
}
}
last = Some(char);
}
normalized
}

/// Returns `true` if the name is already normalized.
fn is_normalized(name: impl AsRef<str>) -> bool {
let mut last = None;
for char in name.as_ref().bytes() {
match char {
b'A'..=b'Z' => {
// Uppercase characters need to be converted to lowercase.
return false;
}
b'_' | b'.' => {
// `_` and `.` are normalized to `-`.
return false;
}
b'-' => {
if matches!(last, Some(b'-')) {
// Runs of `-` are normalized to a single `-`.
return false;
}
}
_ => {}
}
last = Some(char);
}
true
}
}

impl Display for DistInfoName<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl AsRef<str> for DistInfoName<'_> {
fn as_ref(&self) -> &str {
&self.0
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn normalize() {
let inputs = [
"friendly-bard",
"Friendly-Bard",
"FRIENDLY-BARD",
"friendly.bard",
"friendly_bard",
"friendly--bard",
"friendly-.bard",
"FrIeNdLy-._.-bArD",
];
for input in inputs {
assert_eq!(DistInfoName::normalize(input), "friendly-bard");
}
}
}
13 changes: 8 additions & 5 deletions crates/uv-normalize/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::error::Error;
use std::fmt::{Display, Formatter};

pub use dist_info_name::DistInfoName;
pub use extra_name::ExtraName;
pub use group_name::{GroupName, DEV_DEPENDENCIES};
pub use package_name::PackageName;

mod dist_info_name;
mod extra_name;
mod group_name;
mod package_name;
Expand All @@ -22,10 +24,11 @@ pub(crate) fn validate_and_normalize_owned(name: String) -> Result<String, Inval
pub(crate) fn validate_and_normalize_ref(
name: impl AsRef<str>,
) -> Result<String, InvalidNameError> {
let mut normalized = String::with_capacity(name.as_ref().len());
let name = name.as_ref();
let mut normalized = String::with_capacity(name.len());

let mut last = None;
for char in name.as_ref().bytes() {
for char in name.bytes() {
match char {
b'A'..=b'Z' => {
normalized.push(char.to_ascii_lowercase() as char);
Expand All @@ -36,19 +39,19 @@ pub(crate) fn validate_and_normalize_ref(
b'-' | b'_' | b'.' => {
match last {
// Names can't start with punctuation.
None => return Err(InvalidNameError(name.as_ref().to_string())),
None => return Err(InvalidNameError(name.to_string())),
Some(b'-' | b'_' | b'.') => {}
Some(_) => normalized.push('-'),
}
}
_ => return Err(InvalidNameError(name.as_ref().to_string())),
_ => return Err(InvalidNameError(name.to_string())),
}
last = Some(char);
}

// Names can't end with punctuation.
if matches!(last, Some(b'-' | b'_' | b'.')) {
return Err(InvalidNameError(name.as_ref().to_string()));
return Err(InvalidNameError(name.to_string()));
}

Ok(normalized)
Expand Down
Loading