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

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

Merged
merged 1 commit into from
Aug 18, 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
45 changes: 25 additions & 20 deletions crates/distribution-filename/src/egg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand All @@ -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
Expand All @@ -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 })
}
}
Expand All @@ -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());
}
}
68 changes: 50 additions & 18 deletions crates/distribution-types/src/installed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})));
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Loading