diff --git a/crates/distribution-filename/src/egg.rs b/crates/distribution-filename/src/egg.rs index b247a4d40978..79ff9cb918ab 100644 --- a/crates/distribution-filename/src/egg.rs +++ b/crates/distribution-filename/src/egg.rs @@ -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, } 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 { // 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()); } } diff --git a/crates/distribution-types/src/installed.rs b/crates/distribution-types/src/installed.rs index c608ba9a7d68..ecbab4addd89 100644 --- a/crates/distribution-types/src/installed.rs +++ b/crates/distribution-types/src/installed.rs @@ -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 { + 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) +}