Lift requirement that .egg-info filenames must include version (#6179)

## Summary

PR #4533 introduced (almost) spec compliant parsing of `.egg-info`
filenames, but added the overly strict requirement that the distribution
version must be present. This causes various `uv pip` operations to fail
in environments where there are `.egg-info` files without a version
component, so loosen this check by making the version component optional
and reading the version from the egg metadata when it is not present.

As an example of the issue, running `uv pip list` on my system currently
results in
```
error: Failed to read metadata from: `/usr/lib/python3.12/site-packages/PySide6.egg-info`
  Caused by: The `.egg-info` filename "PySide6.egg-info" is missing a version
```
whereas regular `pip list` succeeds:
```
$ pip list | rg -S pyside
PySide6                   6.7.2
```

## Test Plan

This has been tested by altering the `.egg-info` filename tests as
needed and ensuring the full test suite passes locally.
This commit is contained in:
Severen Redwood 2024-08-19 05:04:40 +12:00 committed by GitHub
parent 53159b5d98
commit f8bda467fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 75 additions and 38 deletions

View file

@ -11,8 +11,6 @@ pub enum EggInfoFilenameError {
InvalidExtension(String),
#[error("The `.egg-info` filename \"{0}\" is missing a package name")]
MissingPackageName(String),
#[error("The `.egg-info` filename \"{0}\" is missing a version")]
MissingVersion(String),
#[error("The `.egg-info` filename \"{0}\" has an invalid package name")]
InvalidPackageName(String, InvalidNameError),
#[error("The `.egg-info` filename \"{0}\" has an invalid version: {1}")]
@ -31,11 +29,11 @@ pub enum EggInfoFilenameError {
#[derive(Debug, Clone)]
pub struct EggInfoFilename {
pub name: PackageName,
pub version: Version,
pub version: Option<Version>,
}
impl EggInfoFilename {
/// Parse an `.egg-info` filename, requiring at least a name and version.
/// Parse an `.egg-info` filename, requiring at least a name.
pub fn parse(stem: &str) -> Result<Self, EggInfoFilenameError> {
// pip uses the following regex:
// ```python
@ -56,13 +54,16 @@ impl EggInfoFilename {
let name = parts
.next()
.ok_or_else(|| EggInfoFilenameError::MissingPackageName(format!("{stem}.egg-info")))?;
let version = parts
.next()
.ok_or_else(|| EggInfoFilenameError::MissingVersion(format!("{stem}.egg-info")))?;
let name = PackageName::from_str(name)
.map_err(|e| EggInfoFilenameError::InvalidPackageName(format!("{stem}.egg-info"), e))?;
let version = Version::from_str(version)
.map_err(|e| EggInfoFilenameError::InvalidVersion(format!("{stem}.egg-info"), e))?;
let version = parts
.next()
.map(|s| {
Version::from_str(s).map_err(|e| {
EggInfoFilenameError::InvalidVersion(format!("{stem}.egg-info"), e)
})
})
.transpose()?;
Ok(Self { name, version })
}
}
@ -87,26 +88,30 @@ mod tests {
let filename = "zstandard-0.22.0-py3.12-darwin.egg-info";
let parsed = EggInfoFilename::from_str(filename).unwrap();
assert_eq!(parsed.name.as_ref(), "zstandard");
assert_eq!(parsed.version.to_string(), "0.22.0");
assert_eq!(
parsed.version.map(|v| v.to_string()),
Some("0.22.0".to_string())
);
let filename = "zstandard-0.22.0-py3.12.egg-info";
let parsed = EggInfoFilename::from_str(filename).unwrap();
assert_eq!(parsed.name.as_ref(), "zstandard");
assert_eq!(parsed.version.to_string(), "0.22.0");
assert_eq!(
parsed.version.map(|v| v.to_string()),
Some("0.22.0".to_string())
);
let filename = "zstandard-0.22.0.egg-info";
let parsed = EggInfoFilename::from_str(filename).unwrap();
assert_eq!(parsed.name.as_ref(), "zstandard");
assert_eq!(parsed.version.to_string(), "0.22.0");
}
#[test]
fn egg_info_filename_missing_version() {
let filename = "zstandard.egg-info";
let err = EggInfoFilename::from_str(filename).unwrap_err();
assert_eq!(
err.to_string(),
"The `.egg-info` filename \"zstandard.egg-info\" is missing a version"
parsed.version.map(|v| v.to_string()),
Some("0.22.0".to_string())
);
let filename = "zstandard.egg-info";
let parsed = EggInfoFilename::from_str(filename).unwrap();
assert_eq!(parsed.name.as_ref(), "zstandard");
assert!(parsed.version.is_none());
}
}

View file

@ -136,18 +136,42 @@ impl InstalledDist {
};
let file_name = EggInfoFilename::parse(file_stem)?;
if let Some(version) = file_name.version {
if metadata.is_dir() {
return Ok(Some(Self::EggInfoDirectory(InstalledEggInfoDirectory {
name: file_name.name,
version,
path: path.to_path_buf(),
})));
}
if metadata.is_file() {
return Ok(Some(Self::EggInfoFile(InstalledEggInfoFile {
name: file_name.name,
version,
path: path.to_path_buf(),
})));
}
};
if metadata.is_dir() {
let Some(egg_metadata) = read_metadata(&path.join("PKG-INFO")) else {
return Ok(None);
};
return Ok(Some(Self::EggInfoDirectory(InstalledEggInfoDirectory {
name: file_name.name,
version: file_name.version,
version: Version::from_str(&egg_metadata.version)?,
path: path.to_path_buf(),
})));
}
if metadata.is_file() {
return Ok(Some(Self::EggInfoFile(InstalledEggInfoFile {
let Some(egg_metadata) = read_metadata(path) else {
return Ok(None);
};
return Ok(Some(Self::EggInfoDirectory(InstalledEggInfoDirectory {
name: file_name.name,
version: file_name.version,
version: Version::from_str(&egg_metadata.version)?,
path: path.to_path_buf(),
})));
}
@ -189,24 +213,13 @@ impl InstalledDist {
.map_err(|()| anyhow!("Invalid `.egg-link` target: {}", target.user_display()))?;
// Mildly unfortunate that we must read metadata to get the version.
let content = match fs::read(egg_info.join("PKG-INFO")) {
Ok(content) => content,
Err(err) => {
warn!("Failed to read metadata for {path:?}: {err}");
return Ok(None);
}
};
let metadata = match pypi_types::Metadata10::parse_pkg_info(&content) {
Ok(metadata) => metadata,
Err(err) => {
warn!("Failed to parse metadata for {path:?}: {err}");
return Ok(None);
}
let Some(egg_metadata) = read_metadata(&egg_info.join("PKG-INFO")) else {
return Ok(None);
};
return Ok(Some(Self::LegacyEditable(InstalledLegacyEditable {
name: metadata.name,
version: Version::from_str(&metadata.version)?,
name: egg_metadata.name,
version: Version::from_str(&egg_metadata.version)?,
egg_link: path.to_path_buf(),
target,
target_url: url,
@ -411,3 +424,22 @@ impl InstalledMetadata for InstalledDist {
}
}
}
fn read_metadata(path: &Path) -> Option<pypi_types::Metadata10> {
let content = match fs::read(path) {
Ok(content) => content,
Err(err) => {
warn!("Failed to read metadata for {path:?}: {err}");
return None;
}
};
let metadata = match pypi_types::Metadata10::parse_pkg_info(&content) {
Ok(metadata) => metadata,
Err(err) => {
warn!("Failed to parse metadata for {path:?}: {err}");
return None;
}
};
Some(metadata)
}