Refactor distribution types to adhere to a clear hierarchy (#369)

## Summary

This PR refactors our `RemoteDistribution` type such that it now follows
a clear hierarchy that matches the actual variants, and encodes the
differences between source and built distributions:

```rust
pub enum Distribution {
    Built(BuiltDistribution),
    Source(SourceDistribution),
}

pub enum BuiltDistribution {
    Registry(RegistryBuiltDistribution),
    DirectUrl(DirectUrlBuiltDistribution),
}

pub enum SourceDistribution {
    Registry(RegistrySourceDistribution),
    DirectUrl(DirectUrlSourceDistribution),
    Git(GitSourceDistribution),
}

/// A built distribution (wheel) that exists in a registry, like `PyPI`.
pub struct RegistryBuiltDistribution {
    pub name: PackageName,
    pub version: Version,
    pub file: File,
}

/// A built distribution (wheel) that exists at an arbitrary URL.
pub struct DirectUrlBuiltDistribution {
    pub name: PackageName,
    pub url: Url,
}

/// A source distribution that exists in a registry, like `PyPI`.
pub struct RegistrySourceDistribution {
    pub name: PackageName,
    pub version: Version,
    pub file: File,
}

/// A source distribution that exists at an arbitrary URL.
pub struct DirectUrlSourceDistribution {
    pub name: PackageName,
    pub url: Url,
}

/// A source distribution that exists in a Git repository.
pub struct GitSourceDistribution {
    pub name: PackageName,
    pub url: Url,
}
```

Most of the PR just stems downstream from this change. There are no
behavioral changes, so I'm largely relying on lint, tests, and the
compiler for correctness.
This commit is contained in:
Charlie Marsh 2023-11-09 18:45:41 -08:00 committed by GitHub
parent 33c0901a28
commit a148f9d0be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1729 additions and 1098 deletions

View file

@ -0,0 +1,50 @@
use puffin_normalize::PackageName;
use crate::cached::CachedDistribution;
use crate::installed::InstalledDistribution;
use crate::traits::BaseDistribution;
use crate::{Distribution, VersionOrUrl};
/// A distribution which either exists remotely or locally.
#[derive(Debug, Clone)]
pub enum AnyDistribution {
Remote(Distribution),
Cached(CachedDistribution),
Installed(InstalledDistribution),
}
impl BaseDistribution for AnyDistribution {
fn name(&self) -> &PackageName {
match self {
Self::Remote(dist) => dist.name(),
Self::Cached(dist) => dist.name(),
Self::Installed(dist) => dist.name(),
}
}
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Remote(dist) => dist.version_or_url(),
Self::Cached(dist) => dist.version_or_url(),
Self::Installed(dist) => dist.version_or_url(),
}
}
}
impl From<Distribution> for AnyDistribution {
fn from(dist: Distribution) -> Self {
Self::Remote(dist)
}
}
impl From<CachedDistribution> for AnyDistribution {
fn from(dist: CachedDistribution) -> Self {
Self::Cached(dist)
}
}
impl From<InstalledDistribution> for AnyDistribution {
fn from(dist: InstalledDistribution) -> Self {
Self::Installed(dist)
}
}

View file

@ -0,0 +1,161 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use url::Url;
use crate::traits::BaseDistribution;
use crate::{BuiltDistribution, Distribution, SourceDistribution, VersionOrUrl};
use pep440_rs::Version;
use puffin_normalize::PackageName;
use crate::direct_url::DirectUrl;
/// A built distribution (wheel) that exists in a local cache.
#[derive(Debug, Clone)]
pub enum CachedDistribution {
/// The distribution exists in a registry, like `PyPI`.
Registry(CachedRegistryDistribution),
/// The distribution exists at an arbitrary URL.
Url(CachedDirectUrlDistribution),
}
#[derive(Debug, Clone)]
pub struct CachedRegistryDistribution {
pub name: PackageName,
pub version: Version,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct CachedDirectUrlDistribution {
pub name: PackageName,
pub url: Url,
pub path: PathBuf,
}
impl BaseDistribution for CachedRegistryDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Version(&self.version)
}
}
impl BaseDistribution for CachedDirectUrlDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Url(&self.url)
}
}
impl BaseDistribution for CachedDistribution {
fn name(&self) -> &PackageName {
match self {
Self::Registry(dist) => dist.name(),
Self::Url(dist) => dist.name(),
}
}
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(dist) => dist.version_or_url(),
Self::Url(dist) => dist.version_or_url(),
}
}
}
impl CachedDistribution {
/// Initialize a [`CachedDistribution`] from a [`Distribution`].
pub fn from_remote(remote: Distribution, path: PathBuf) -> Self {
match remote {
Distribution::Built(BuiltDistribution::Registry(dist)) => {
Self::Registry(CachedRegistryDistribution {
name: dist.name,
version: dist.version,
path,
})
}
Distribution::Built(BuiltDistribution::DirectUrl(dist)) => {
Self::Url(CachedDirectUrlDistribution {
name: dist.name,
url: dist.url,
path,
})
}
Distribution::Source(SourceDistribution::Registry(dist)) => {
Self::Registry(CachedRegistryDistribution {
name: dist.name,
version: dist.version,
path,
})
}
Distribution::Source(SourceDistribution::DirectUrl(dist)) => {
Self::Url(CachedDirectUrlDistribution {
name: dist.name,
url: dist.url,
path,
})
}
Distribution::Source(SourceDistribution::Git(dist)) => {
Self::Url(CachedDirectUrlDistribution {
name: dist.name,
url: dist.url,
path,
})
}
}
}
/// Return the [`Path`] at which the distribution is stored on-disk.
pub fn path(&self) -> &Path {
match self {
Self::Registry(dist) => &dist.path,
Self::Url(dist) => &dist.path,
}
}
/// Return the [`DirectUrl`] of the distribution, if it exists.
pub fn direct_url(&self) -> Result<Option<DirectUrl>> {
match self {
CachedDistribution::Registry(_) => Ok(None),
CachedDistribution::Url(dist) => DirectUrl::try_from(&dist.url).map(Some),
}
}
}
impl CachedDirectUrlDistribution {
pub fn from_url(name: PackageName, url: Url, path: PathBuf) -> Self {
Self { name, url, path }
}
}
impl CachedRegistryDistribution {
/// Try to parse a distribution from a cached directory name (like `django-5.0a1`).
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
let Some(file_name) = path.file_name() else {
return Ok(None);
};
let Some(file_name) = file_name.to_str() else {
return Ok(None);
};
let Some((name, version)) = file_name.rsplit_once('-') else {
return Ok(None);
};
let name = PackageName::from_str(name)?;
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
let path = path.to_path_buf();
Ok(Some(Self {
name,
version,
path,
}))
}
}

View file

@ -0,0 +1,188 @@
use std::path::PathBuf;
use anyhow::{Context, Error, Result};
use url::Url;
use puffin_git::GitUrl;
#[derive(Debug)]
pub enum DirectUrl {
Git(DirectGitUrl),
Archive(DirectArchiveUrl),
}
#[derive(Debug)]
pub struct DirectGitUrl {
pub url: GitUrl,
pub subdirectory: Option<PathBuf>,
}
#[derive(Debug)]
pub struct DirectArchiveUrl {
pub url: Url,
pub subdirectory: Option<PathBuf>,
}
impl TryFrom<&Url> for DirectGitUrl {
type Error = Error;
fn try_from(url: &Url) -> Result<Self, Self::Error> {
// If the URL points to a subdirectory, extract it, as in:
// `https://git.example.com/MyProject.git@v1.0#subdirectory=pkg_dir`
// `https://git.example.com/MyProject.git@v1.0#egg=pkg&subdirectory=pkg_dir`
let subdirectory = url.fragment().and_then(|fragment| {
fragment
.split('&')
.find_map(|fragment| fragment.strip_prefix("subdirectory=").map(PathBuf::from))
});
let url = url
.as_str()
.strip_prefix("git+")
.context("Missing git+ prefix for Git URL")?;
let url = Url::parse(url)?;
let url = GitUrl::try_from(url)?;
Ok(Self { url, subdirectory })
}
}
impl From<&Url> for DirectArchiveUrl {
fn from(url: &Url) -> Self {
// If the URL points to a subdirectory, extract it, as in:
// `https://git.example.com/MyProject.git@v1.0#subdirectory=pkg_dir`
// `https://git.example.com/MyProject.git@v1.0#egg=pkg&subdirectory=pkg_dir`
let subdirectory = url.fragment().and_then(|fragment| {
fragment
.split('&')
.find_map(|fragment| fragment.strip_prefix("subdirectory=").map(PathBuf::from))
});
let url = url.clone();
Self { url, subdirectory }
}
}
impl TryFrom<&Url> for DirectUrl {
type Error = Error;
fn try_from(url: &Url) -> Result<Self, Self::Error> {
if let Some((prefix, ..)) = url.scheme().split_once('+') {
match prefix {
"git" => Ok(Self::Git(DirectGitUrl::try_from(url)?)),
_ => Err(Error::msg(format!(
"Unsupported URL prefix `{prefix}` in URL: {url}",
))),
}
} else {
Ok(Self::Archive(DirectArchiveUrl::from(url)))
}
}
}
impl TryFrom<&DirectUrl> for pypi_types::DirectUrl {
type Error = Error;
fn try_from(value: &DirectUrl) -> std::result::Result<Self, Self::Error> {
match value {
DirectUrl::Git(value) => pypi_types::DirectUrl::try_from(value),
DirectUrl::Archive(value) => pypi_types::DirectUrl::try_from(value),
}
}
}
impl TryFrom<&DirectArchiveUrl> for pypi_types::DirectUrl {
type Error = Error;
fn try_from(value: &DirectArchiveUrl) -> Result<Self, Self::Error> {
Ok(pypi_types::DirectUrl::ArchiveUrl {
url: value.url.to_string(),
archive_info: pypi_types::ArchiveInfo {
hash: None,
hashes: None,
},
subdirectory: value.subdirectory.clone(),
})
}
}
impl TryFrom<&DirectGitUrl> for pypi_types::DirectUrl {
type Error = Error;
fn try_from(value: &DirectGitUrl) -> Result<Self, Self::Error> {
Ok(pypi_types::DirectUrl::VcsUrl {
url: value.url.repository().to_string(),
vcs_info: pypi_types::VcsInfo {
vcs: pypi_types::VcsKind::Git,
commit_id: value.url.precise().map(|oid| oid.to_string()),
requested_revision: value.url.reference().map(ToString::to_string),
},
subdirectory: value.subdirectory.clone(),
})
}
}
impl From<DirectUrl> for Url {
fn from(value: DirectUrl) -> Self {
match value {
DirectUrl::Git(value) => value.into(),
DirectUrl::Archive(value) => value.into(),
}
}
}
impl From<DirectArchiveUrl> for Url {
fn from(value: DirectArchiveUrl) -> Self {
let mut url = value.url;
if let Some(subdirectory) = value.subdirectory {
url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
}
url
}
}
impl From<DirectGitUrl> for Url {
fn from(value: DirectGitUrl) -> Self {
let mut url = Url::parse(&format!("{}{}", "git+", Url::from(value.url).as_str()))
.expect("Git URL is invalid");
if let Some(subdirectory) = value.subdirectory {
url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
}
url
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use url::Url;
use crate::direct_url::DirectUrl;
#[test]
fn direct_url_from_url() -> Result<()> {
let expected = Url::parse("git+https://github.com/pallets/flask.git")?;
let actual = Url::from(DirectUrl::try_from(&expected)?);
assert_eq!(expected, actual);
let expected = Url::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?;
let actual = Url::from(DirectUrl::try_from(&expected)?);
assert_eq!(expected, actual);
let expected = Url::parse("git+https://github.com/pallets/flask.git@2.0.0")?;
let actual = Url::from(DirectUrl::try_from(&expected)?);
assert_eq!(expected, actual);
let expected =
Url::parse("git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir")?;
let actual = Url::from(DirectUrl::try_from(&expected)?);
assert_eq!(expected, actual);
// TODO(charlie): Preserve other fragments.
let expected =
Url::parse("git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir")?;
let actual = Url::from(DirectUrl::try_from(&expected)?);
assert_ne!(expected, actual);
Ok(())
}
}

View file

@ -0,0 +1,133 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use pep440_rs::Version;
use puffin_normalize::PackageName;
use pypi_types::DirectUrl;
use crate::{BaseDistribution, VersionOrUrl};
/// A built distribution (wheel) that exists in a virtual environment.
#[derive(Debug, Clone)]
pub enum InstalledDistribution {
/// The distribution was derived from a registry, like `PyPI`.
Registry(InstalledRegistryDistribution),
/// The distribution was derived from an arbitrary URL.
Url(InstalledDirectUrlDistribution),
}
#[derive(Debug, Clone)]
pub struct InstalledRegistryDistribution {
pub name: PackageName,
pub version: Version,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct InstalledDirectUrlDistribution {
pub name: PackageName,
pub version: Version,
pub url: DirectUrl,
pub path: PathBuf,
}
impl BaseDistribution for InstalledRegistryDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Version(&self.version)
}
}
impl BaseDistribution for InstalledDirectUrlDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
// TODO(charlie): Convert a `DirectUrl` to `Url`.
VersionOrUrl::Version(&self.version)
}
}
impl BaseDistribution for InstalledDistribution {
fn name(&self) -> &PackageName {
match self {
Self::Registry(dist) => dist.name(),
Self::Url(dist) => dist.name(),
}
}
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(dist) => dist.version_or_url(),
Self::Url(dist) => dist.version_or_url(),
}
}
}
impl InstalledDistribution {
/// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`).
///
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
if path.extension().is_some_and(|ext| ext == "dist-info") {
let Some(file_stem) = path.file_stem() else {
return Ok(None);
};
let Some(file_stem) = file_stem.to_str() else {
return Ok(None);
};
let Some((name, version)) = file_stem.split_once('-') else {
return Ok(None);
};
let name = PackageName::from_str(name)?;
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
return if let Some(direct_url) = Self::direct_url(path)? {
Ok(Some(Self::Url(InstalledDirectUrlDistribution {
name,
version,
url: direct_url,
path: path.to_path_buf(),
})))
} else {
Ok(Some(Self::Registry(InstalledRegistryDistribution {
name,
version,
path: path.to_path_buf(),
})))
};
}
Ok(None)
}
/// Return the [`Path`] at which the distribution is stored on-disk.
pub fn path(&self) -> &Path {
match self {
Self::Registry(dist) => &dist.path,
Self::Url(dist) => &dist.path,
}
}
pub fn version(&self) -> &Version {
match self {
Self::Registry(dist) => &dist.version,
Self::Url(dist) => &dist.version,
}
}
/// Read the `direct_url.json` file from a `.dist-info` directory.
fn direct_url(path: &Path) -> Result<Option<DirectUrl>> {
let path = path.join("direct_url.json");
let Ok(file) = fs_err::File::open(path) else {
return Ok(None);
};
let direct_url = serde_json::from_reader::<fs_err::File, DirectUrl>(file)?;
Ok(Some(direct_url))
}
}

View file

@ -1,63 +1,22 @@
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::path::Path;
use anyhow::{anyhow, Result};
use anyhow::{Context, Result};
use url::Url;
use pep440_rs::Version;
use puffin_cache::CanonicalUrl;
use puffin_normalize::PackageName;
use pypi_types::{DirectUrl, File};
use pypi_types::File;
pub mod source;
pub use crate::any::*;
pub use crate::cached::*;
pub use crate::installed::*;
pub use crate::traits::*;
/// A built distribution (wheel), which either exists remotely or locally.
#[derive(Debug, Clone)]
pub enum Distribution {
Remote(RemoteDistribution),
Cached(CachedDistribution),
Installed(InstalledDistribution),
}
impl Distribution {
/// Return the normalized [`PackageName`] of the distribution.
pub fn name(&self) -> &PackageName {
match self {
Self::Remote(dist) => dist.name(),
Self::Cached(dist) => dist.name(),
Self::Installed(dist) => dist.name(),
}
}
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
pub fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Remote(dist) => dist.version_or_url(),
Self::Cached(dist) => dist.version_or_url(),
Self::Installed(dist) => dist.version_or_url(),
}
}
}
impl From<RemoteDistribution> for Distribution {
fn from(dist: RemoteDistribution) -> Self {
Self::Remote(dist)
}
}
impl From<CachedDistribution> for Distribution {
fn from(dist: CachedDistribution) -> Self {
Self::Cached(dist)
}
}
impl From<InstalledDistribution> for Distribution {
fn from(dist: InstalledDistribution) -> Self {
Self::Installed(dist)
}
}
mod any;
mod cached;
pub mod direct_url;
mod installed;
mod traits;
#[derive(Debug, Clone)]
pub enum VersionOrUrl<'a> {
@ -76,361 +35,446 @@ impl std::fmt::Display for VersionOrUrl<'_> {
}
}
/// A built distribution (wheel) that exists as a remote file (e.g., on `PyPI`).
#[derive(Debug, Clone)]
pub enum Distribution {
Built(BuiltDistribution),
Source(SourceDistribution),
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum RemoteDistribution {
/// The distribution exists in a registry, like `PyPI`.
Registry(PackageName, Version, File),
/// The distribution exists at an arbitrary URL.
Url(PackageName, Url),
pub enum BuiltDistribution {
Registry(RegistryBuiltDistribution),
DirectUrl(DirectUrlBuiltDistribution),
}
impl RemoteDistribution {
/// Create a [`RemoteDistribution`] for a registry-based distribution.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum SourceDistribution {
Registry(RegistrySourceDistribution),
DirectUrl(DirectUrlSourceDistribution),
Git(GitSourceDistribution),
}
/// A built distribution (wheel) that exists in a registry, like `PyPI`.
#[derive(Debug, Clone)]
pub struct RegistryBuiltDistribution {
pub name: PackageName,
pub version: Version,
pub file: File,
}
/// A built distribution (wheel) that exists at an arbitrary URL.
#[derive(Debug, Clone)]
pub struct DirectUrlBuiltDistribution {
pub name: PackageName,
pub url: Url,
}
/// A source distribution that exists in a registry, like `PyPI`.
#[derive(Debug, Clone)]
pub struct RegistrySourceDistribution {
pub name: PackageName,
pub version: Version,
pub file: File,
}
/// A source distribution that exists at an arbitrary URL.
#[derive(Debug, Clone)]
pub struct DirectUrlSourceDistribution {
pub name: PackageName,
pub url: Url,
}
/// A source distribution that exists in a Git repository.
#[derive(Debug, Clone)]
pub struct GitSourceDistribution {
pub name: PackageName,
pub url: Url,
}
impl Distribution {
/// Create a [`Distribution`] for a registry-based distribution.
pub fn from_registry(name: PackageName, version: Version, file: File) -> Self {
Self::Registry(name, version, file)
}
/// Create a [`RemoteDistribution`] for a URL-based distribution.
pub fn from_url(name: PackageName, url: Url) -> Self {
Self::Url(name, url)
}
/// Return the URL of the distribution.
pub fn url(&self) -> Result<Cow<'_, Url>> {
match self {
Self::Registry(_, _, file) => {
let url = Url::parse(&file.url)?;
Ok(Cow::Owned(url))
}
Self::Url(_, url) => Ok(Cow::Borrowed(url)),
}
}
/// Return the filename of the distribution.
pub fn filename(&self) -> Result<Cow<'_, str>> {
match self {
Self::Registry(_, _, file) => Ok(Cow::Borrowed(&file.filename)),
Self::Url(_, url) => {
let filename = url
.path_segments()
.and_then(Iterator::last)
.ok_or_else(|| anyhow!("Could not parse filename from URL: {}", url))?;
Ok(Cow::Owned(filename.to_owned()))
}
}
}
/// Return the normalized [`PackageName`] of the distribution.
pub fn name(&self) -> &PackageName {
match self {
Self::Registry(name, _, _) => name,
Self::Url(name, _) => name,
}
}
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
pub fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(_, version, _) => VersionOrUrl::Version(version),
Self::Url(_, url) => VersionOrUrl::Url(url),
}
}
/// Returns a unique identifier for this distribution.
pub fn id(&self) -> String {
match self {
Self::Registry(name, version, _) => {
// https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-dist-info-directory
// `version` is normalized by its `ToString` impl
format!(
"{}-{}",
PackageName::from(name).as_dist_info_name(),
version
)
}
Self::Url(_name, url) => puffin_cache::digest(&CanonicalUrl::new(url)),
}
}
/// Returns `true` if this distribution is a wheel.
pub fn is_wheel(&self) -> bool {
let filename = match self {
Self::Registry(_name, _version, file) => &file.filename,
Self::Url(_name, url) => url.path(),
};
Path::new(filename)
if Path::new(&file.filename)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
}
}
impl std::fmt::Display for RemoteDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Registry(name, version, _file) => {
write!(f, "{name}=={version}")
}
Self::Url(name, url) => {
write!(f, "{name} @ {url}")
}
}
}
}
/// A built distribution (wheel) that exists in a local cache.
#[derive(Debug, Clone)]
pub enum CachedDistribution {
/// The distribution exists in a registry, like `PyPI`.
Registry(PackageName, Version, PathBuf),
/// The distribution exists at an arbitrary URL.
Url(PackageName, Url, PathBuf),
}
impl CachedDistribution {
/// Initialize a [`CachedDistribution`] from a [`RemoteDistribution`].
pub fn from_remote(remote: RemoteDistribution, path: PathBuf) -> Self {
match remote {
RemoteDistribution::Registry(name, version, _file) => {
Self::Registry(name, version, path)
}
RemoteDistribution::Url(name, url) => Self::Url(name, url, path),
}
}
/// Try to parse a distribution from a cached directory name (like `django-5.0a1`).
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
let Some(file_name) = path.file_name() else {
return Ok(None);
};
let Some(file_name) = file_name.to_str() else {
return Ok(None);
};
let Some((name, version)) = file_name.split_once('-') else {
return Ok(None);
};
let name = PackageName::from_str(name)?;
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
let path = path.to_path_buf();
Ok(Some(Self::Registry(name, version, path)))
}
/// Return the normalized [`PackageName`] of the distribution.
pub fn name(&self) -> &PackageName {
match self {
Self::Registry(name, _, _) => name,
Self::Url(name, _, _) => name,
}
}
/// Return the [`Path`] at which the distribution is stored on-disk.
pub fn path(&self) -> &Path {
match self {
Self::Registry(_, _, path) => path,
Self::Url(_, _, path) => path,
}
}
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
pub fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(_, version, _) => VersionOrUrl::Version(version),
Self::Url(_, url, _) => VersionOrUrl::Url(url),
}
}
}
impl std::fmt::Display for CachedDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Registry(name, version, _file) => {
write!(f, "{name}=={version}")
}
Self::Url(name, url, _path) => {
write!(f, "{name} @ {url}")
}
}
}
}
/// A built distribution (wheel) that exists in a virtual environment.
#[derive(Debug, Clone)]
pub struct InstalledDistribution {
name: PackageName,
version: Version,
path: PathBuf,
}
impl InstalledDistribution {
/// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`).
///
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
if path.extension().is_some_and(|ext| ext == "dist-info") {
let Some(file_stem) = path.file_stem() else {
return Ok(None);
};
let Some(file_stem) = file_stem.to_str() else {
return Ok(None);
};
let Some((name, version)) = file_stem.split_once('-') else {
return Ok(None);
};
let name = PackageName::from_str(name)?;
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
let path = path.to_path_buf();
return Ok(Some(Self {
{
Self::Built(BuiltDistribution::Registry(RegistryBuiltDistribution {
name,
version,
path,
}));
file,
}))
} else {
Self::Source(SourceDistribution::Registry(RegistrySourceDistribution {
name,
version,
file,
}))
}
Ok(None)
}
/// Return the normalized [`PackageName`] of the distribution.
pub fn name(&self) -> &PackageName {
/// Create a [`Distribution`] for a URL-based distribution.
pub fn from_url(name: PackageName, url: Url) -> Self {
if url.scheme().starts_with("git+") {
Self::Source(SourceDistribution::Git(GitSourceDistribution { name, url }))
} else if Path::new(url.path())
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
{
Self::Built(BuiltDistribution::DirectUrl(DirectUrlBuiltDistribution {
name,
url,
}))
} else {
Self::Source(SourceDistribution::DirectUrl(DirectUrlSourceDistribution {
name,
url,
}))
}
}
}
impl BaseDistribution for RegistryBuiltDistribution {
fn name(&self) -> &PackageName {
&self.name
}
/// Return the [`Version`] of the distribution.
pub fn version(&self) -> &Version {
&self.version
}
/// Return the [`Path`] at which the distribution is stored on-disk.
pub fn path(&self) -> &Path {
&self.path
}
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
pub fn version_or_url(&self) -> VersionOrUrl {
// TODO(charlie): If this dependency was installed via a direct URL, return it here, rather
// than the version.
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Version(&self.version)
}
}
/// Return the [`DirectUrl`] metadata for this distribution, if it exists.
pub fn direct_url(&self) -> Result<Option<DirectUrl>> {
let path = self.path.join("direct_url.json");
let Ok(file) = fs_err::File::open(path) else {
return Ok(None);
};
let direct_url = serde_json::from_reader::<fs_err::File, DirectUrl>(file)?;
Ok(Some(direct_url))
impl BaseDistribution for DirectUrlBuiltDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Url(&self.url)
}
}
impl std::fmt::Display for InstalledDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}=={}", self.name(), self.version())
impl BaseDistribution for RegistrySourceDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Version(&self.version)
}
}
/// Unowned reference to a [`RemoteDistribution`].
#[derive(Debug, Clone)]
pub enum RemoteDistributionRef<'a> {
/// The distribution exists in a registry, like `PyPI`.
Registry(&'a PackageName, &'a Version, &'a File),
/// The distribution exists at an arbitrary URL.
Url(&'a PackageName, &'a Url),
impl BaseDistribution for DirectUrlSourceDistribution {
fn name(&self) -> &PackageName {
&self.name
}
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Url(&self.url)
}
}
impl<'a> RemoteDistributionRef<'a> {
/// Create a [`RemoteDistribution`] for a registry-based distribution.
pub fn from_registry(name: &'a PackageName, version: &'a Version, file: &'a File) -> Self {
Self::Registry(name, version, file)
impl BaseDistribution for GitSourceDistribution {
fn name(&self) -> &PackageName {
&self.name
}
/// Create a [`RemoteDistribution`] for a URL-based distribution.
pub fn from_url(name: &'a PackageName, url: &'a Url) -> Self {
Self::Url(name, url)
fn version_or_url(&self) -> VersionOrUrl {
VersionOrUrl::Url(&self.url)
}
}
/// Return the URL of the distribution.
pub fn url(&self) -> Result<Cow<'_, Url>> {
impl BaseDistribution for SourceDistribution {
fn name(&self) -> &PackageName {
match self {
Self::Registry(_, _, file) => {
let url = Url::parse(&file.url)?;
Ok(Cow::Owned(url))
}
Self::Url(_, url) => Ok(Cow::Borrowed(url)),
Self::Registry(dist) => dist.name(),
Self::DirectUrl(dist) => dist.name(),
Self::Git(dist) => dist.name(),
}
}
/// Return the filename of the distribution.
pub fn filename(&self) -> Result<Cow<'_, str>> {
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(_, _, file) => Ok(Cow::Borrowed(&file.filename)),
Self::Url(_, url) => {
let filename = url
.path_segments()
.and_then(std::iter::Iterator::last)
.ok_or_else(|| anyhow!("Could not parse filename from URL: {}", url))?;
Ok(Cow::Owned(filename.to_owned()))
}
}
}
/// Return the normalized [`PackageName`] of the distribution.
pub fn name(&self) -> &PackageName {
match self {
Self::Registry(name, _, _) => name,
Self::Url(name, _) => name,
}
}
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
pub fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(_, version, _) => VersionOrUrl::Version(version),
Self::Url(_, url) => VersionOrUrl::Url(url),
}
}
/// Returns a unique identifier for this distribution.
pub fn id(&self) -> String {
match self {
Self::Registry(name, version, _) => {
// https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-dist-info-directory
// `version` is normalized by its `ToString` impl
format!("{}-{}", PackageName::from(*name), version)
}
Self::Url(_name, url) => puffin_cache::digest(&CanonicalUrl::new(url)),
Self::Registry(dist) => dist.version_or_url(),
Self::DirectUrl(dist) => dist.version_or_url(),
Self::Git(dist) => dist.version_or_url(),
}
}
}
impl std::fmt::Display for RemoteDistributionRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl BaseDistribution for BuiltDistribution {
fn name(&self) -> &PackageName {
match self {
Self::Registry(name, version, _file) => {
write!(f, "{name}=={version}")
}
Self::Url(name, url) => {
write!(f, "{name} @ {url}")
}
Self::Registry(dist) => dist.name(),
Self::DirectUrl(dist) => dist.name(),
}
}
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Registry(dist) => dist.version_or_url(),
Self::DirectUrl(dist) => dist.version_or_url(),
}
}
}
impl<'a> From<&'a RemoteDistribution> for RemoteDistributionRef<'a> {
fn from(dist: &'a RemoteDistribution) -> Self {
match dist {
RemoteDistribution::Registry(name, version, file) => {
Self::Registry(name, version, file)
}
RemoteDistribution::Url(name, url) => Self::Url(name, url),
impl BaseDistribution for Distribution {
fn name(&self) -> &PackageName {
match self {
Self::Built(dist) => dist.name(),
Self::Source(dist) => dist.name(),
}
}
fn version_or_url(&self) -> VersionOrUrl {
match self {
Self::Built(dist) => dist.version_or_url(),
Self::Source(dist) => dist.version_or_url(),
}
}
}
impl RemoteDistribution for RegistryBuiltDistribution {
fn filename(&self) -> Result<&str> {
Ok(&self.file.filename)
}
fn size(&self) -> Option<usize> {
Some(self.file.size)
}
}
impl RemoteDistribution for RegistrySourceDistribution {
fn filename(&self) -> Result<&str> {
Ok(&self.file.filename)
}
fn size(&self) -> Option<usize> {
Some(self.file.size)
}
}
impl RemoteDistribution for DirectUrlBuiltDistribution {
fn filename(&self) -> Result<&str> {
self.url
.path_segments()
.and_then(Iterator::last)
.map(|filename| {
filename
.rsplit_once('@')
.map_or(filename, |(_, filename)| filename)
})
.with_context(|| format!("Could not parse filename from URL: {}", self.url))
}
fn size(&self) -> Option<usize> {
None
}
}
impl RemoteDistribution for DirectUrlSourceDistribution {
fn filename(&self) -> Result<&str> {
self.url
.path_segments()
.and_then(Iterator::last)
.map(|filename| {
filename
.rsplit_once('@')
.map_or(filename, |(_, filename)| filename)
})
.with_context(|| format!("Could not parse filename from URL: {}", self.url))
}
fn size(&self) -> Option<usize> {
None
}
}
impl RemoteDistribution for GitSourceDistribution {
fn filename(&self) -> Result<&str> {
self.url
.path_segments()
.and_then(Iterator::last)
.map(|filename| {
filename
.rsplit_once('@')
.map_or(filename, |(_, filename)| filename)
})
.with_context(|| format!("Could not parse filename from URL: {}", self.url))
}
fn size(&self) -> Option<usize> {
None
}
}
impl RemoteDistribution for SourceDistribution {
fn filename(&self) -> Result<&str> {
match self {
Self::Registry(dist) => dist.filename(),
Self::DirectUrl(dist) => dist.filename(),
Self::Git(dist) => dist.filename(),
}
}
fn size(&self) -> Option<usize> {
match self {
Self::Registry(dist) => dist.size(),
Self::DirectUrl(dist) => dist.size(),
Self::Git(dist) => dist.size(),
}
}
}
impl RemoteDistribution for BuiltDistribution {
fn filename(&self) -> Result<&str> {
match self {
Self::Registry(dist) => dist.filename(),
Self::DirectUrl(dist) => dist.filename(),
}
}
fn size(&self) -> Option<usize> {
match self {
Self::Registry(dist) => dist.size(),
Self::DirectUrl(dist) => dist.size(),
}
}
}
impl RemoteDistribution for Distribution {
fn filename(&self) -> Result<&str> {
match self {
Self::Built(dist) => dist.filename(),
Self::Source(dist) => dist.filename(),
}
}
fn size(&self) -> Option<usize> {
match self {
Self::Built(dist) => dist.size(),
Self::Source(dist) => dist.size(),
}
}
}
impl DistributionIdentifier for Url {
fn distribution_id(&self) -> String {
puffin_cache::digest(&puffin_cache::CanonicalUrl::new(self))
}
fn resource_id(&self) -> String {
puffin_cache::digest(&puffin_cache::RepositoryUrl::new(self))
}
}
impl DistributionIdentifier for File {
fn distribution_id(&self) -> String {
self.hashes.sha256.clone()
}
fn resource_id(&self) -> String {
self.hashes.sha256.clone()
}
}
impl DistributionIdentifier for RegistryBuiltDistribution {
fn distribution_id(&self) -> String {
self.file.distribution_id()
}
fn resource_id(&self) -> String {
self.file.resource_id()
}
}
impl DistributionIdentifier for RegistrySourceDistribution {
fn distribution_id(&self) -> String {
self.file.distribution_id()
}
fn resource_id(&self) -> String {
self.file.resource_id()
}
}
impl DistributionIdentifier for DirectUrlBuiltDistribution {
fn distribution_id(&self) -> String {
self.url.distribution_id()
}
fn resource_id(&self) -> String {
self.url.resource_id()
}
}
impl DistributionIdentifier for DirectUrlSourceDistribution {
fn distribution_id(&self) -> String {
self.url.distribution_id()
}
fn resource_id(&self) -> String {
self.url.resource_id()
}
}
impl DistributionIdentifier for GitSourceDistribution {
fn distribution_id(&self) -> String {
self.url.distribution_id()
}
fn resource_id(&self) -> String {
self.url.resource_id()
}
}
impl DistributionIdentifier for SourceDistribution {
fn distribution_id(&self) -> String {
match self {
Self::Registry(dist) => dist.distribution_id(),
Self::DirectUrl(dist) => dist.distribution_id(),
Self::Git(dist) => dist.distribution_id(),
}
}
fn resource_id(&self) -> String {
match self {
Self::Registry(dist) => dist.resource_id(),
Self::DirectUrl(dist) => dist.resource_id(),
Self::Git(dist) => dist.resource_id(),
}
}
}
impl DistributionIdentifier for BuiltDistribution {
fn distribution_id(&self) -> String {
match self {
Self::Registry(dist) => dist.distribution_id(),
Self::DirectUrl(dist) => dist.distribution_id(),
}
}
fn resource_id(&self) -> String {
match self {
Self::Registry(dist) => dist.resource_id(),
Self::DirectUrl(dist) => dist.resource_id(),
}
}
}
impl DistributionIdentifier for Distribution {
fn distribution_id(&self) -> String {
match self {
Self::Built(dist) => dist.distribution_id(),
Self::Source(dist) => dist.distribution_id(),
}
}
fn resource_id(&self) -> String {
match self {
Self::Built(dist) => dist.resource_id(),
Self::Source(dist) => dist.resource_id(),
}
}
}

View file

@ -1,116 +0,0 @@
use std::path::PathBuf;
use anyhow::{anyhow, Error, Result};
use url::Url;
use puffin_git::Git;
use pypi_types::{ArchiveInfo, DirectUrl, VcsInfo, VcsKind};
use crate::RemoteDistributionRef;
/// The source of a distribution.
#[derive(Debug)]
pub enum Source<'a> {
/// The distribution is available at a URL in a registry, like PyPI.
RegistryUrl(Url),
/// The distribution is available at an arbitrary remote URL, like a GitHub Release.
RemoteUrl(&'a Url, Option<PathBuf>),
/// The distribution is available in a remote Git repository.
Git(Git, Option<PathBuf>),
}
impl<'a> TryFrom<&'a RemoteDistributionRef<'_>> for Source<'a> {
type Error = Error;
fn try_from(value: &'a RemoteDistributionRef<'_>) -> Result<Self, Self::Error> {
match value {
// If a distribution is hosted on a registry, it must be available at a URL.
RemoteDistributionRef::Registry(_, _, file) => {
Ok(Self::RegistryUrl(Url::parse(&file.url)?))
}
// If a distribution is specified via a direct URL, it could be a URL to a hosted file,
// or a URL to a Git repository.
RemoteDistributionRef::Url(_, url) => Self::try_from(*url),
}
}
}
impl<'a> TryFrom<&'a Url> for Source<'a> {
type Error = Error;
fn try_from(url: &'a Url) -> Result<Self, Self::Error> {
// If the URL points to a subdirectory, extract it, as in:
// `https://git.example.com/MyProject.git@v1.0#subdirectory=pkg_dir`
// `https://git.example.com/MyProject.git@v1.0#egg=pkg&subdirectory=pkg_dir`
let subdirectory = url.fragment().and_then(|fragment| {
fragment
.split('&')
.find_map(|fragment| fragment.strip_prefix("subdirectory=").map(PathBuf::from))
});
// If a distribution is specified via a direct URL, it could be a URL to a hosted file,
// or a URL to a Git repository.
if let Some(url) = url.as_str().strip_prefix("git+") {
let url = Url::parse(url)?;
let git = Git::try_from(url)?;
Ok(Self::Git(git, subdirectory))
} else {
Ok(Self::RemoteUrl(url, subdirectory))
}
}
}
impl From<Source<'_>> for Url {
fn from(value: Source) -> Self {
match value {
Source::RegistryUrl(url) => url,
Source::RemoteUrl(url, subdirectory) => {
if let Some(subdirectory) = subdirectory {
let mut url = (*url).clone();
url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
url
} else {
url.clone()
}
}
Source::Git(git, subdirectory) => {
let mut url = Url::parse(&format!("{}{}", "git+", Url::from(git).as_str()))
.expect("git url is valid");
if let Some(subdirectory) = subdirectory {
url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
}
url
}
}
}
}
impl TryFrom<Source<'_>> for DirectUrl {
type Error = Error;
fn try_from(value: Source<'_>) -> Result<Self, Self::Error> {
match value {
Source::RegistryUrl(_) => Err(anyhow!("Registry dependencies have no direct URL")),
Source::RemoteUrl(url, subdirectory) => Ok(DirectUrl::ArchiveUrl {
url: url.to_string(),
archive_info: ArchiveInfo {
hash: None,
hashes: None,
},
subdirectory,
}),
Source::Git(git, subdirectory) => Ok(DirectUrl::VcsUrl {
url: git.url().to_string(),
vcs_info: VcsInfo {
vcs: VcsKind::Git,
// TODO(charlie): In `pip-sync`, we should `.precise` our Git dependencies,
// even though we expect it to be a no-op.
commit_id: git.precise().map(|oid| oid.to_string()),
requested_revision: git.reference().map(ToString::to_string),
},
subdirectory,
}),
}
}
}

View file

@ -0,0 +1,149 @@
use anyhow::Result;
use puffin_cache::CanonicalUrl;
use puffin_normalize::PackageName;
use crate::{
AnyDistribution, BuiltDistribution, CachedDirectUrlDistribution, CachedDistribution,
CachedRegistryDistribution, DirectUrlBuiltDistribution, DirectUrlSourceDistribution,
Distribution, GitSourceDistribution, InstalledDirectUrlDistribution, InstalledDistribution,
InstalledRegistryDistribution, RegistryBuiltDistribution, RegistrySourceDistribution,
SourceDistribution, VersionOrUrl,
};
pub trait BaseDistribution {
/// Return the normalized [`PackageName`] of the distribution.
fn name(&self) -> &PackageName;
/// Return a [`Version`], for registry-based distributions, or a [`Url`], for URL-based
/// distributions.
fn version_or_url(&self) -> VersionOrUrl;
/// Returns a unique identifier for the package.
///
/// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple
/// registry-based distributions (e.g., different wheels for the same package and version)
/// will return the same package ID, but different distribution IDs.
fn package_id(&self) -> String {
match self.version_or_url() {
VersionOrUrl::Version(version) => {
// https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-dist-info-directory
// `version` is normalized by its `ToString` impl
format!("{}-{}", self.name().as_dist_info_name(), version)
}
VersionOrUrl::Url(url) => puffin_cache::digest(&CanonicalUrl::new(url)),
}
}
}
pub trait RemoteDistribution {
/// Return an appropriate filename for the distribution.
fn filename(&self) -> Result<&str>;
/// Return the size of the distribution, if known.
fn size(&self) -> Option<usize>;
}
pub trait DistributionIdentifier {
/// Return a unique resource identifier for the distribution, like a SHA-256 hash of the
/// distribution's contents.
fn distribution_id(&self) -> String;
/// Return a unique resource identifier for the underlying resource backing the distribution.
///
/// This is often equivalent to the distribution ID, but may differ in some cases. For example,
/// if the same Git repository is used for two different distributions, at two different
/// subdirectories or two different commits, then those distributions would share a resource ID,
/// but have different distribution IDs.
fn resource_id(&self) -> String;
}
// Implement `Display` for all known types that implement `DistributionIdentifier`.
impl std::fmt::Display for AnyDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for BuiltDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for CachedDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for CachedDirectUrlDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for CachedRegistryDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for DirectUrlBuiltDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for DirectUrlSourceDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for Distribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for GitSourceDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for InstalledDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for InstalledDirectUrlDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for InstalledRegistryDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for RegistryBuiltDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for RegistrySourceDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}
impl std::fmt::Display for SourceDistribution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.name(), self.version_or_url())
}
}