Skip to main content

uv_distribution_types/
lib.rs

1//! ## Type hierarchy
2//!
3//! When we receive the requirements from `pip sync`, we check which requirements already fulfilled
4//! in the users environment ([`InstalledDist`]), whether the matching package is in our wheel cache
5//! ([`CachedDist`]) or whether we need to download, (potentially build) and install it ([`Dist`]).
6//! These three variants make up [`BuiltDist`].
7//!
8//! ## `Dist`
9//! A [`Dist`] is either a built distribution (a wheel), or a source distribution that exists at
10//! some location. We translate every PEP 508 requirement e.g. from `requirements.txt` or from
11//! `pyproject.toml`'s `[project] dependencies` into a [`Dist`] by checking each index.
12//! * [`BuiltDist`]: A wheel, with its three possible origins:
13//!   * [`RegistryBuiltDist`]
14//!   * [`DirectUrlBuiltDist`]
15//!   * [`PathBuiltDist`]
16//! * [`SourceDist`]: A source distribution, with its four possible origins:
17//!   * [`RegistrySourceDist`]
18//!   * [`DirectUrlSourceDist`]
19//!   * [`GitSourceDist`]
20//!   * [`PathSourceDist`]
21//!
22//! ## `CachedDist`
23//! A [`CachedDist`] is a built distribution (wheel) that exists in the local cache, with the two
24//! possible origins we currently track:
25//! * [`CachedRegistryDist`]
26//! * [`CachedDirectUrlDist`]
27//!
28//! ## `InstalledDist`
29//! An [`InstalledDist`] is built distribution (wheel) that is installed in a virtual environment,
30//! with the two possible origins we currently track:
31//! * [`InstalledRegistryDist`]
32//! * [`InstalledDirectUrlDist`]
33//!
34//! Since we read this information from [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/), it doesn't match the information [`Dist`] exactly.
35use std::borrow::Cow;
36use std::path;
37use std::path::Path;
38use std::str::FromStr;
39
40use url::Url;
41
42use uv_distribution_filename::{
43    DistExtension, SourceDistExtension, SourceDistFilename, WheelFilename,
44};
45use uv_fs::normalize_absolute_path;
46use uv_git_types::GitUrl;
47use uv_normalize::PackageName;
48use uv_pep440::Version;
49use uv_pep508::{Pep508Url, VerbatimUrl};
50use uv_pypi_types::{
51    ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl,
52};
53use uv_redacted::DisplaySafeUrl;
54
55pub use crate::annotation::*;
56pub use crate::any::*;
57pub use crate::build_info::*;
58pub use crate::build_requires::*;
59pub use crate::buildable::*;
60pub use crate::cached::*;
61pub use crate::config_settings::*;
62pub use crate::dependency_metadata::*;
63pub use crate::diagnostic::*;
64pub use crate::dist_error::*;
65pub use crate::error::*;
66pub use crate::file::*;
67pub use crate::hash::*;
68pub use crate::id::*;
69pub use crate::index::*;
70pub use crate::index_name::*;
71pub use crate::index_url::*;
72pub use crate::installed::*;
73pub use crate::known_platform::*;
74pub use crate::origin::*;
75pub use crate::pip_index::*;
76pub use crate::prioritized_distribution::*;
77pub use crate::requested::*;
78pub use crate::requirement::*;
79pub use crate::requires_python::*;
80pub use crate::resolution::*;
81pub use crate::resolved::*;
82pub use crate::specified_requirement::*;
83pub use crate::status_code_strategy::*;
84pub use crate::traits::*;
85
86mod annotation;
87mod any;
88mod build_info;
89mod build_requires;
90mod buildable;
91mod cached;
92mod config_settings;
93mod dependency_metadata;
94mod diagnostic;
95mod dist_error;
96mod error;
97mod file;
98mod hash;
99mod id;
100mod index;
101mod index_name;
102mod index_url;
103mod installed;
104mod known_platform;
105mod origin;
106mod pip_index;
107mod prioritized_distribution;
108mod requested;
109mod requirement;
110mod requires_python;
111mod resolution;
112mod resolved;
113mod specified_requirement;
114mod status_code_strategy;
115mod traits;
116
117#[derive(Debug, Clone)]
118pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> {
119    /// A PEP 440 version specifier, used to identify a distribution in a registry.
120    Version(&'a Version),
121    /// A URL, used to identify a distribution at an arbitrary location.
122    Url(&'a T),
123}
124
125impl<'a, T: Pep508Url> VersionOrUrlRef<'a, T> {
126    /// If it is a URL, return its value.
127    pub fn url(&self) -> Option<&'a T> {
128        match self {
129            Self::Version(_) => None,
130            Self::Url(url) => Some(url),
131        }
132    }
133}
134
135impl Verbatim for VersionOrUrlRef<'_> {
136    fn verbatim(&self) -> Cow<'_, str> {
137        match self {
138            Self::Version(version) => Cow::Owned(format!("=={version}")),
139            Self::Url(url) => Cow::Owned(format!(" @ {}", url.verbatim())),
140        }
141    }
142}
143
144impl std::fmt::Display for VersionOrUrlRef<'_> {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::Version(version) => write!(f, "=={version}"),
148            Self::Url(url) => write!(f, " @ {url}"),
149        }
150    }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
154pub enum InstalledVersion<'a> {
155    /// A PEP 440 version specifier, used to identify a distribution in a registry.
156    Version(&'a Version),
157    /// A URL, used to identify a distribution at an arbitrary location, along with the version
158    /// specifier to which it resolved.
159    Url(&'a DisplaySafeUrl, &'a Version),
160}
161
162impl<'a> InstalledVersion<'a> {
163    /// If it is a URL, return its value.
164    pub fn url(&self) -> Option<&'a DisplaySafeUrl> {
165        match self {
166            Self::Version(_) => None,
167            Self::Url(url, _) => Some(url),
168        }
169    }
170
171    /// If it is a version, return its value.
172    pub fn version(&self) -> &'a Version {
173        match self {
174            Self::Version(version) => version,
175            Self::Url(_, version) => version,
176        }
177    }
178}
179
180impl std::fmt::Display for InstalledVersion<'_> {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            Self::Version(version) => write!(f, "=={version}"),
184            Self::Url(url, version) => write!(f, "=={version} (from {url})"),
185        }
186    }
187}
188
189/// Either a built distribution, a wheel, or a source distribution that exists at some location.
190///
191/// The location can be an index, URL or path (wheel), or index, URL, path or Git repository (source distribution).
192#[derive(Debug, Clone, Hash, PartialEq, Eq)]
193pub enum Dist {
194    Built(BuiltDist),
195    Source(SourceDist),
196}
197
198/// A reference to a built or source distribution.
199#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
200pub enum DistRef<'a> {
201    Built(&'a BuiltDist),
202    Source(&'a SourceDist),
203}
204
205/// A wheel, with its three possible origins (index, url, path)
206#[derive(Debug, Clone, Hash, PartialEq, Eq)]
207pub enum BuiltDist {
208    Registry(RegistryBuiltDist),
209    DirectUrl(DirectUrlBuiltDist),
210    Path(PathBuiltDist),
211}
212
213/// A source distribution, with its possible origins (index, url, path, git)
214#[derive(Debug, Clone, Hash, PartialEq, Eq)]
215pub enum SourceDist {
216    Registry(RegistrySourceDist),
217    DirectUrl(DirectUrlSourceDist),
218    Git(GitSourceDist),
219    Path(PathSourceDist),
220    Directory(DirectorySourceDist),
221}
222
223/// A built distribution (wheel) that exists in a registry, like `PyPI`.
224#[derive(Debug, Clone, Hash, PartialEq, Eq)]
225pub struct RegistryBuiltWheel {
226    pub filename: WheelFilename,
227    pub file: Box<File>,
228    pub index: IndexUrl,
229}
230
231/// A built distribution (wheel) that exists in a registry, like `PyPI`.
232#[derive(Debug, Clone, Hash, PartialEq, Eq)]
233pub struct RegistryBuiltDist {
234    /// All wheels associated with this distribution. It is guaranteed
235    /// that there is at least one wheel.
236    pub wheels: Vec<RegistryBuiltWheel>,
237    /// The "best" wheel selected based on the current wheel tag
238    /// environment.
239    ///
240    /// This is guaranteed to point into a valid entry in `wheels`.
241    pub best_wheel_index: usize,
242    /// A source distribution if one exists for this distribution.
243    ///
244    /// It is possible for this to be `None`. For example, when a distribution
245    /// has no source distribution, or if it does have one but isn't compatible
246    /// with the user configuration. (e.g., If `Requires-Python` isn't
247    /// compatible with the installed/target Python versions, or if something
248    /// like `--exclude-newer` was used.)
249    pub sdist: Option<RegistrySourceDist>,
250    // Ideally, this type would have an index URL on it, and the
251    // `RegistryBuiltDist` and `RegistrySourceDist` types would *not* have an
252    // index URL on them. Alas, the --find-links feature makes it technically
253    // possible for the indexes to diverge across wheels/sdists in the same
254    // distribution.
255    //
256    // Note though that at time of writing, when generating a universal lock
257    // file, we require that all index URLs across wheels/sdists for a single
258    // distribution are equivalent.
259}
260
261/// A built distribution (wheel) that exists at an arbitrary URL.
262#[derive(Debug, Clone, Hash, PartialEq, Eq)]
263pub struct DirectUrlBuiltDist {
264    /// We require that wheel urls end in the full wheel filename, e.g.
265    /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl`
266    pub filename: WheelFilename,
267    /// The URL without the subdirectory fragment.
268    pub location: Box<DisplaySafeUrl>,
269    /// The URL as it was provided by the user.
270    pub url: VerbatimUrl,
271}
272
273/// A built distribution (wheel) that exists in a local directory.
274#[derive(Debug, Clone, Hash, PartialEq, Eq)]
275pub struct PathBuiltDist {
276    pub filename: WheelFilename,
277    /// The absolute path to the wheel which we use for installing.
278    pub install_path: Box<Path>,
279    /// The URL as it was provided by the user.
280    pub url: VerbatimUrl,
281}
282
283/// A source distribution that exists in a registry, like `PyPI`.
284#[derive(Debug, Clone, Hash, PartialEq, Eq)]
285pub struct RegistrySourceDist {
286    pub name: PackageName,
287    pub version: Version,
288    pub file: Box<File>,
289    /// The file extension, e.g. `tar.gz`, `zip`, etc.
290    pub ext: SourceDistExtension,
291    pub index: IndexUrl,
292    /// When an sdist is selected, it may be the case that there were
293    /// available wheels too. There are many reasons why a wheel might not
294    /// have been chosen (maybe none available are compatible with the
295    /// current environment), but we still want to track that they exist. In
296    /// particular, for generating a universal lockfile, we do not want to
297    /// skip emitting wheels to the lockfile just because the host generating
298    /// the lockfile didn't have any compatible wheels available.
299    pub wheels: Vec<RegistryBuiltWheel>,
300}
301
302/// A source distribution that exists at an arbitrary URL.
303#[derive(Debug, Clone, Hash, PartialEq, Eq)]
304pub struct DirectUrlSourceDist {
305    /// Unlike [`DirectUrlBuiltDist`], we can't require a full filename with a version here, people
306    /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip`
307    pub name: PackageName,
308    /// The URL without the subdirectory fragment.
309    pub location: Box<DisplaySafeUrl>,
310    /// The subdirectory within the archive in which the source distribution is located.
311    pub subdirectory: Option<Box<Path>>,
312    /// The file extension, e.g. `tar.gz`, `zip`, etc.
313    pub ext: SourceDistExtension,
314    /// The URL as it was provided by the user, including the subdirectory fragment.
315    pub url: VerbatimUrl,
316}
317
318/// A source distribution that exists in a Git repository.
319#[derive(Debug, Clone, Hash, PartialEq, Eq)]
320pub struct GitSourceDist {
321    pub name: PackageName,
322    /// The URL without the revision and subdirectory fragment.
323    pub git: Box<GitUrl>,
324    /// The subdirectory within the Git repository in which the source distribution is located.
325    pub subdirectory: Option<Box<Path>>,
326    /// The URL as it was provided by the user, including the revision and subdirectory fragment.
327    pub url: VerbatimUrl,
328}
329
330/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file).
331#[derive(Debug, Clone, Hash, PartialEq, Eq)]
332pub struct PathSourceDist {
333    pub name: PackageName,
334    pub version: Option<Version>,
335    /// The absolute path to the distribution which we use for installing.
336    pub install_path: Box<Path>,
337    /// The file extension, e.g. `tar.gz`, `zip`, etc.
338    pub ext: SourceDistExtension,
339    /// The URL as it was provided by the user.
340    pub url: VerbatimUrl,
341}
342
343/// A source distribution that exists in a local directory.
344#[derive(Debug, Clone, Hash, PartialEq, Eq)]
345pub struct DirectorySourceDist {
346    pub name: PackageName,
347    /// The absolute path to the distribution which we use for installing.
348    pub install_path: Box<Path>,
349    /// Whether the package should be installed in editable mode.
350    pub editable: Option<bool>,
351    /// Whether the package should be built and installed.
352    pub r#virtual: Option<bool>,
353    /// The URL as it was provided by the user.
354    pub url: VerbatimUrl,
355}
356
357impl Dist {
358    /// A remote built distribution (`.whl`) or source distribution from a `http://` or `https://`
359    /// URL.
360    pub fn from_http_url(
361        name: PackageName,
362        url: VerbatimUrl,
363        location: DisplaySafeUrl,
364        subdirectory: Option<Box<Path>>,
365        ext: DistExtension,
366    ) -> Result<Self, Error> {
367        match ext {
368            DistExtension::Wheel => {
369                // Validate that the name in the wheel matches that of the requirement.
370                let filename = WheelFilename::from_str(&url.filename()?)?;
371                if filename.name != name {
372                    return Err(Error::PackageNameMismatch(
373                        name,
374                        filename.name,
375                        url.verbatim().to_string(),
376                    ));
377                }
378
379                Ok(Self::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
380                    filename,
381                    location: Box::new(location),
382                    url,
383                })))
384            }
385            DistExtension::Source(ext) => {
386                Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
387                    name,
388                    location: Box::new(location),
389                    subdirectory,
390                    ext,
391                    url,
392                })))
393            }
394        }
395    }
396
397    /// A local built or source distribution from a `file://` URL.
398    pub fn from_file_url(
399        name: PackageName,
400        url: VerbatimUrl,
401        install_path: &Path,
402        ext: DistExtension,
403    ) -> Result<Self, Error> {
404        // Convert to an absolute path.
405        let install_path = path::absolute(install_path)?;
406
407        // Normalize the path.
408        let install_path = normalize_absolute_path(&install_path)?;
409
410        // Validate that the path exists.
411        if !install_path.exists() {
412            return Err(Error::NotFound(url.to_url()));
413        }
414
415        // Determine whether the path represents a built or source distribution.
416        match ext {
417            DistExtension::Wheel => {
418                // Validate that the name in the wheel matches that of the requirement.
419                let filename = WheelFilename::from_str(&url.filename()?)?;
420                if filename.name != name {
421                    return Err(Error::PackageNameMismatch(
422                        name,
423                        filename.name,
424                        url.verbatim().to_string(),
425                    ));
426                }
427                Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
428                    filename,
429                    install_path: install_path.into_boxed_path(),
430                    url,
431                })))
432            }
433            DistExtension::Source(ext) => {
434                // If there is a version in the filename, record it.
435                let version = url
436                    .filename()
437                    .ok()
438                    .and_then(|filename| {
439                        SourceDistFilename::parse(filename.as_ref(), ext, &name).ok()
440                    })
441                    .map(|filename| filename.version);
442
443                Ok(Self::Source(SourceDist::Path(PathSourceDist {
444                    name,
445                    version,
446                    install_path: install_path.into_boxed_path(),
447                    ext,
448                    url,
449                })))
450            }
451        }
452    }
453
454    /// A local source tree from a `file://` URL.
455    pub fn from_directory_url(
456        name: PackageName,
457        url: VerbatimUrl,
458        install_path: &Path,
459        editable: Option<bool>,
460        r#virtual: Option<bool>,
461    ) -> Result<Self, Error> {
462        // Convert to an absolute path.
463        let install_path = path::absolute(install_path)?;
464
465        // Normalize the path.
466        let install_path = normalize_absolute_path(&install_path)?;
467
468        // Validate that the path exists.
469        if !install_path.exists() {
470            return Err(Error::NotFound(url.to_url()));
471        }
472
473        // Determine whether the path represents an archive or a directory.
474        Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
475            name,
476            install_path: install_path.into_boxed_path(),
477            editable,
478            r#virtual,
479            url,
480        })))
481    }
482
483    /// A remote source distribution from a `git+https://` or `git+ssh://` url.
484    pub fn from_git_url(
485        name: PackageName,
486        url: VerbatimUrl,
487        git: GitUrl,
488        subdirectory: Option<Box<Path>>,
489    ) -> Result<Self, Error> {
490        Ok(Self::Source(SourceDist::Git(GitSourceDist {
491            name,
492            git: Box::new(git),
493            subdirectory,
494            url,
495        })))
496    }
497
498    /// Create a [`Dist`] for a URL-based distribution.
499    pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result<Self, Error> {
500        match url.parsed_url {
501            ParsedUrl::Archive(archive) => Self::from_http_url(
502                name,
503                url.verbatim,
504                archive.url,
505                archive.subdirectory,
506                archive.ext,
507            ),
508            ParsedUrl::Path(file) => {
509                Self::from_file_url(name, url.verbatim, &file.install_path, file.ext)
510            }
511            ParsedUrl::Directory(directory) => Self::from_directory_url(
512                name,
513                url.verbatim,
514                &directory.install_path,
515                directory.editable,
516                directory.r#virtual,
517            ),
518            ParsedUrl::Git(git) => {
519                Self::from_git_url(name, url.verbatim, git.url, git.subdirectory)
520            }
521        }
522    }
523
524    /// Return true if the distribution is editable.
525    pub fn is_editable(&self) -> bool {
526        match self {
527            Self::Source(dist) => dist.is_editable(),
528            Self::Built(_) => false,
529        }
530    }
531
532    /// Return true if the distribution refers to a local file or directory.
533    pub fn is_local(&self) -> bool {
534        match self {
535            Self::Source(dist) => dist.is_local(),
536            Self::Built(dist) => dist.is_local(),
537        }
538    }
539
540    /// Returns the [`IndexUrl`], if the distribution is from a registry.
541    pub fn index(&self) -> Option<&IndexUrl> {
542        match self {
543            Self::Built(dist) => dist.index(),
544            Self::Source(dist) => dist.index(),
545        }
546    }
547
548    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
549    pub fn file(&self) -> Option<&File> {
550        match self {
551            Self::Built(built) => built.file(),
552            Self::Source(source) => source.file(),
553        }
554    }
555
556    /// Return the source tree of the distribution, if available.
557    pub fn source_tree(&self) -> Option<&Path> {
558        match self {
559            Self::Built { .. } => None,
560            Self::Source(source) => source.source_tree(),
561        }
562    }
563
564    /// Returns the version of the distribution, if it is known.
565    pub fn version(&self) -> Option<&Version> {
566        match self {
567            Self::Built(wheel) => Some(wheel.version()),
568            Self::Source(source_dist) => source_dist.version(),
569        }
570    }
571
572    /// Convert this distribution into a reference.
573    pub fn as_ref(&self) -> DistRef<'_> {
574        match self {
575            Self::Built(dist) => DistRef::Built(dist),
576            Self::Source(dist) => DistRef::Source(dist),
577        }
578    }
579}
580
581impl<'a> From<&'a Dist> for DistRef<'a> {
582    fn from(dist: &'a Dist) -> Self {
583        match dist {
584            Dist::Built(built) => DistRef::Built(built),
585            Dist::Source(source) => DistRef::Source(source),
586        }
587    }
588}
589
590impl<'a> From<&'a SourceDist> for DistRef<'a> {
591    fn from(dist: &'a SourceDist) -> Self {
592        DistRef::Source(dist)
593    }
594}
595
596impl<'a> From<&'a BuiltDist> for DistRef<'a> {
597    fn from(dist: &'a BuiltDist) -> Self {
598        DistRef::Built(dist)
599    }
600}
601
602impl BuiltDist {
603    /// Return true if the distribution refers to a local file or directory.
604    pub fn is_local(&self) -> bool {
605        matches!(self, Self::Path(_))
606    }
607
608    /// Returns the [`IndexUrl`], if the distribution is from a registry.
609    pub fn index(&self) -> Option<&IndexUrl> {
610        match self {
611            Self::Registry(registry) => Some(&registry.best_wheel().index),
612            Self::DirectUrl(_) => None,
613            Self::Path(_) => None,
614        }
615    }
616
617    /// Returns the [`File`] instance, if this distribution is from a registry.
618    pub fn file(&self) -> Option<&File> {
619        match self {
620            Self::Registry(registry) => Some(&registry.best_wheel().file),
621            Self::DirectUrl(_) | Self::Path(_) => None,
622        }
623    }
624
625    pub fn version(&self) -> &Version {
626        match self {
627            Self::Registry(wheels) => &wheels.best_wheel().filename.version,
628            Self::DirectUrl(wheel) => &wheel.filename.version,
629            Self::Path(wheel) => &wheel.filename.version,
630        }
631    }
632}
633
634impl SourceDist {
635    /// Returns the [`SourceDistExtension`] of the distribution, if it has one.
636    pub fn extension(&self) -> Option<SourceDistExtension> {
637        match self {
638            Self::Registry(source_dist) => Some(source_dist.ext),
639            Self::DirectUrl(source_dist) => Some(source_dist.ext),
640            Self::Path(source_dist) => Some(source_dist.ext),
641            Self::Git(_) | Self::Directory(_) => None,
642        }
643    }
644
645    /// Returns the [`IndexUrl`], if the distribution is from a registry.
646    pub fn index(&self) -> Option<&IndexUrl> {
647        match self {
648            Self::Registry(registry) => Some(&registry.index),
649            Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
650        }
651    }
652
653    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
654    pub fn file(&self) -> Option<&File> {
655        match self {
656            Self::Registry(registry) => Some(&registry.file),
657            Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
658        }
659    }
660
661    /// Returns the [`Version`] of the distribution, if it is known.
662    pub fn version(&self) -> Option<&Version> {
663        match self {
664            Self::Registry(source_dist) => Some(&source_dist.version),
665            Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
666        }
667    }
668
669    /// Returns `true` if the distribution is editable.
670    pub fn is_editable(&self) -> bool {
671        match self {
672            Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false),
673            _ => false,
674        }
675    }
676
677    /// Returns `true` if the distribution is virtual.
678    pub fn is_virtual(&self) -> bool {
679        match self {
680            Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false),
681            _ => false,
682        }
683    }
684
685    /// Returns `true` if the distribution refers to a local file or directory.
686    pub fn is_local(&self) -> bool {
687        matches!(self, Self::Directory(_) | Self::Path(_))
688    }
689
690    /// Returns the path to the source distribution, if it's a local distribution.
691    pub fn as_path(&self) -> Option<&Path> {
692        match self {
693            Self::Path(dist) => Some(&dist.install_path),
694            Self::Directory(dist) => Some(&dist.install_path),
695            _ => None,
696        }
697    }
698
699    /// Returns the source tree of the distribution, if available.
700    pub fn source_tree(&self) -> Option<&Path> {
701        match self {
702            Self::Directory(dist) => Some(&dist.install_path),
703            _ => None,
704        }
705    }
706}
707
708impl RegistryBuiltDist {
709    /// Returns the best or "most compatible" wheel in this distribution.
710    pub fn best_wheel(&self) -> &RegistryBuiltWheel {
711        &self.wheels[self.best_wheel_index]
712    }
713}
714
715impl DirectUrlBuiltDist {
716    /// Return the [`ParsedUrl`] for the distribution.
717    pub fn parsed_url(&self) -> ParsedUrl {
718        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
719            (*self.location).clone(),
720            None,
721            DistExtension::Wheel,
722        ))
723    }
724}
725
726impl PathBuiltDist {
727    /// Return the [`ParsedUrl`] for the distribution.
728    pub fn parsed_url(&self) -> ParsedUrl {
729        ParsedUrl::Path(ParsedPathUrl::from_source(
730            self.install_path.clone(),
731            DistExtension::Wheel,
732            self.url.to_url(),
733        ))
734    }
735}
736
737impl PathSourceDist {
738    /// Return the [`ParsedUrl`] for the distribution.
739    pub fn parsed_url(&self) -> ParsedUrl {
740        ParsedUrl::Path(ParsedPathUrl::from_source(
741            self.install_path.clone(),
742            DistExtension::Source(self.ext),
743            self.url.to_url(),
744        ))
745    }
746}
747
748impl DirectUrlSourceDist {
749    /// Return the [`ParsedUrl`] for the distribution.
750    pub fn parsed_url(&self) -> ParsedUrl {
751        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
752            (*self.location).clone(),
753            self.subdirectory.clone(),
754            DistExtension::Source(self.ext),
755        ))
756    }
757}
758
759impl GitSourceDist {
760    /// Return the [`ParsedUrl`] for the distribution.
761    pub fn parsed_url(&self) -> ParsedUrl {
762        ParsedUrl::Git(ParsedGitUrl::from_source(
763            (*self.git).clone(),
764            self.subdirectory.clone(),
765        ))
766    }
767}
768
769impl DirectorySourceDist {
770    /// Return the [`ParsedUrl`] for the distribution.
771    pub fn parsed_url(&self) -> ParsedUrl {
772        ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
773            self.install_path.clone(),
774            self.editable,
775            self.r#virtual,
776            self.url.to_url(),
777        ))
778    }
779}
780
781impl Name for RegistryBuiltWheel {
782    fn name(&self) -> &PackageName {
783        &self.filename.name
784    }
785}
786
787impl Name for RegistryBuiltDist {
788    fn name(&self) -> &PackageName {
789        self.best_wheel().name()
790    }
791}
792
793impl Name for DirectUrlBuiltDist {
794    fn name(&self) -> &PackageName {
795        &self.filename.name
796    }
797}
798
799impl Name for PathBuiltDist {
800    fn name(&self) -> &PackageName {
801        &self.filename.name
802    }
803}
804
805impl Name for RegistrySourceDist {
806    fn name(&self) -> &PackageName {
807        &self.name
808    }
809}
810
811impl Name for DirectUrlSourceDist {
812    fn name(&self) -> &PackageName {
813        &self.name
814    }
815}
816
817impl Name for GitSourceDist {
818    fn name(&self) -> &PackageName {
819        &self.name
820    }
821}
822
823impl Name for PathSourceDist {
824    fn name(&self) -> &PackageName {
825        &self.name
826    }
827}
828
829impl Name for DirectorySourceDist {
830    fn name(&self) -> &PackageName {
831        &self.name
832    }
833}
834
835impl Name for SourceDist {
836    fn name(&self) -> &PackageName {
837        match self {
838            Self::Registry(dist) => dist.name(),
839            Self::DirectUrl(dist) => dist.name(),
840            Self::Git(dist) => dist.name(),
841            Self::Path(dist) => dist.name(),
842            Self::Directory(dist) => dist.name(),
843        }
844    }
845}
846
847impl Name for BuiltDist {
848    fn name(&self) -> &PackageName {
849        match self {
850            Self::Registry(dist) => dist.name(),
851            Self::DirectUrl(dist) => dist.name(),
852            Self::Path(dist) => dist.name(),
853        }
854    }
855}
856
857impl Name for Dist {
858    fn name(&self) -> &PackageName {
859        match self {
860            Self::Built(dist) => dist.name(),
861            Self::Source(dist) => dist.name(),
862        }
863    }
864}
865
866impl Name for CompatibleDist<'_> {
867    fn name(&self) -> &PackageName {
868        match self {
869            Self::InstalledDist(dist) => dist.name(),
870            Self::SourceDist {
871                sdist,
872                prioritized: _,
873            } => sdist.name(),
874            Self::CompatibleWheel {
875                wheel,
876                priority: _,
877                prioritized: _,
878            } => wheel.name(),
879            Self::IncompatibleWheel {
880                sdist,
881                wheel: _,
882                prioritized: _,
883            } => sdist.name(),
884        }
885    }
886}
887
888impl DistributionMetadata for RegistryBuiltWheel {
889    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
890        VersionOrUrlRef::Version(&self.filename.version)
891    }
892}
893
894impl DistributionMetadata for RegistryBuiltDist {
895    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
896        self.best_wheel().version_or_url()
897    }
898}
899
900impl DistributionMetadata for DirectUrlBuiltDist {
901    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
902        VersionOrUrlRef::Url(&self.url)
903    }
904}
905
906impl DistributionMetadata for PathBuiltDist {
907    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
908        VersionOrUrlRef::Url(&self.url)
909    }
910}
911
912impl DistributionMetadata for RegistrySourceDist {
913    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
914        VersionOrUrlRef::Version(&self.version)
915    }
916}
917
918impl DistributionMetadata for DirectUrlSourceDist {
919    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
920        VersionOrUrlRef::Url(&self.url)
921    }
922}
923
924impl DistributionMetadata for GitSourceDist {
925    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
926        VersionOrUrlRef::Url(&self.url)
927    }
928}
929
930impl DistributionMetadata for PathSourceDist {
931    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
932        VersionOrUrlRef::Url(&self.url)
933    }
934}
935
936impl DistributionMetadata for DirectorySourceDist {
937    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
938        VersionOrUrlRef::Url(&self.url)
939    }
940}
941
942impl DistributionMetadata for SourceDist {
943    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
944        match self {
945            Self::Registry(dist) => dist.version_or_url(),
946            Self::DirectUrl(dist) => dist.version_or_url(),
947            Self::Git(dist) => dist.version_or_url(),
948            Self::Path(dist) => dist.version_or_url(),
949            Self::Directory(dist) => dist.version_or_url(),
950        }
951    }
952}
953
954impl DistributionMetadata for BuiltDist {
955    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
956        match self {
957            Self::Registry(dist) => dist.version_or_url(),
958            Self::DirectUrl(dist) => dist.version_or_url(),
959            Self::Path(dist) => dist.version_or_url(),
960        }
961    }
962}
963
964impl DistributionMetadata for Dist {
965    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
966        match self {
967            Self::Built(dist) => dist.version_or_url(),
968            Self::Source(dist) => dist.version_or_url(),
969        }
970    }
971}
972
973impl RemoteSource for File {
974    fn filename(&self) -> Result<Cow<'_, str>, Error> {
975        Ok(Cow::Borrowed(&self.filename))
976    }
977
978    fn size(&self) -> Option<u64> {
979        self.size
980    }
981}
982
983impl RemoteSource for Url {
984    fn filename(&self) -> Result<Cow<'_, str>, Error> {
985        // Identify the last segment of the URL as the filename.
986        let mut path_segments = self
987            .path_segments()
988            .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?;
989
990        // This is guaranteed by the contract of `Url::path_segments`.
991        let last = path_segments
992            .next_back()
993            .expect("path segments is non-empty");
994
995        // Decode the filename, which may be percent-encoded.
996        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
997
998        Ok(filename)
999    }
1000
1001    fn size(&self) -> Option<u64> {
1002        None
1003    }
1004}
1005
1006impl RemoteSource for UrlString {
1007    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1008        // Take the last segment, stripping any query or fragment.
1009        let last = self
1010            .base_str()
1011            .split('/')
1012            .next_back()
1013            .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?;
1014
1015        // Decode the filename, which may be percent-encoded.
1016        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
1017
1018        Ok(filename)
1019    }
1020
1021    fn size(&self) -> Option<u64> {
1022        None
1023    }
1024}
1025
1026impl RemoteSource for RegistryBuiltWheel {
1027    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1028        self.file.filename()
1029    }
1030
1031    fn size(&self) -> Option<u64> {
1032        self.file.size()
1033    }
1034}
1035
1036impl RemoteSource for RegistryBuiltDist {
1037    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1038        self.best_wheel().filename()
1039    }
1040
1041    fn size(&self) -> Option<u64> {
1042        self.best_wheel().size()
1043    }
1044}
1045
1046impl RemoteSource for RegistrySourceDist {
1047    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1048        self.file.filename()
1049    }
1050
1051    fn size(&self) -> Option<u64> {
1052        self.file.size()
1053    }
1054}
1055
1056impl RemoteSource for DirectUrlBuiltDist {
1057    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1058        self.url.filename()
1059    }
1060
1061    fn size(&self) -> Option<u64> {
1062        self.url.size()
1063    }
1064}
1065
1066impl RemoteSource for DirectUrlSourceDist {
1067    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1068        self.url.filename()
1069    }
1070
1071    fn size(&self) -> Option<u64> {
1072        self.url.size()
1073    }
1074}
1075
1076impl RemoteSource for GitSourceDist {
1077    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1078        // The filename is the last segment of the URL, before any `@`.
1079        match self.url.filename()? {
1080            Cow::Borrowed(filename) => {
1081                if let Some((_, filename)) = filename.rsplit_once('@') {
1082                    Ok(Cow::Borrowed(filename))
1083                } else {
1084                    Ok(Cow::Borrowed(filename))
1085                }
1086            }
1087            Cow::Owned(filename) => {
1088                if let Some((_, filename)) = filename.rsplit_once('@') {
1089                    Ok(Cow::Owned(filename.to_owned()))
1090                } else {
1091                    Ok(Cow::Owned(filename))
1092                }
1093            }
1094        }
1095    }
1096
1097    fn size(&self) -> Option<u64> {
1098        self.url.size()
1099    }
1100}
1101
1102impl RemoteSource for PathBuiltDist {
1103    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1104        self.url.filename()
1105    }
1106
1107    fn size(&self) -> Option<u64> {
1108        self.url.size()
1109    }
1110}
1111
1112impl RemoteSource for PathSourceDist {
1113    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1114        self.url.filename()
1115    }
1116
1117    fn size(&self) -> Option<u64> {
1118        self.url.size()
1119    }
1120}
1121
1122impl RemoteSource for DirectorySourceDist {
1123    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1124        self.url.filename()
1125    }
1126
1127    fn size(&self) -> Option<u64> {
1128        self.url.size()
1129    }
1130}
1131
1132impl RemoteSource for SourceDist {
1133    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1134        match self {
1135            Self::Registry(dist) => dist.filename(),
1136            Self::DirectUrl(dist) => dist.filename(),
1137            Self::Git(dist) => dist.filename(),
1138            Self::Path(dist) => dist.filename(),
1139            Self::Directory(dist) => dist.filename(),
1140        }
1141    }
1142
1143    fn size(&self) -> Option<u64> {
1144        match self {
1145            Self::Registry(dist) => dist.size(),
1146            Self::DirectUrl(dist) => dist.size(),
1147            Self::Git(dist) => dist.size(),
1148            Self::Path(dist) => dist.size(),
1149            Self::Directory(dist) => dist.size(),
1150        }
1151    }
1152}
1153
1154impl RemoteSource for BuiltDist {
1155    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1156        match self {
1157            Self::Registry(dist) => dist.filename(),
1158            Self::DirectUrl(dist) => dist.filename(),
1159            Self::Path(dist) => dist.filename(),
1160        }
1161    }
1162
1163    fn size(&self) -> Option<u64> {
1164        match self {
1165            Self::Registry(dist) => dist.size(),
1166            Self::DirectUrl(dist) => dist.size(),
1167            Self::Path(dist) => dist.size(),
1168        }
1169    }
1170}
1171
1172impl RemoteSource for Dist {
1173    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1174        match self {
1175            Self::Built(dist) => dist.filename(),
1176            Self::Source(dist) => dist.filename(),
1177        }
1178    }
1179
1180    fn size(&self) -> Option<u64> {
1181        match self {
1182            Self::Built(dist) => dist.size(),
1183            Self::Source(dist) => dist.size(),
1184        }
1185    }
1186}
1187
1188impl Identifier for DisplaySafeUrl {
1189    fn distribution_id(&self) -> DistributionId {
1190        DistributionId::Url(uv_cache_key::CanonicalUrl::new(self))
1191    }
1192
1193    fn resource_id(&self) -> ResourceId {
1194        ResourceId::Url(uv_cache_key::RepositoryUrl::new(self))
1195    }
1196}
1197
1198impl Identifier for File {
1199    fn distribution_id(&self) -> DistributionId {
1200        self.hashes
1201            .first()
1202            .cloned()
1203            .map(DistributionId::Digest)
1204            .unwrap_or_else(|| self.url.distribution_id())
1205    }
1206
1207    fn resource_id(&self) -> ResourceId {
1208        self.hashes
1209            .first()
1210            .cloned()
1211            .map(ResourceId::Digest)
1212            .unwrap_or_else(|| self.url.resource_id())
1213    }
1214}
1215
1216impl Identifier for Path {
1217    fn distribution_id(&self) -> DistributionId {
1218        DistributionId::PathBuf(self.to_path_buf())
1219    }
1220
1221    fn resource_id(&self) -> ResourceId {
1222        ResourceId::PathBuf(self.to_path_buf())
1223    }
1224}
1225
1226impl Identifier for FileLocation {
1227    fn distribution_id(&self) -> DistributionId {
1228        match self {
1229            Self::RelativeUrl(base, url) => {
1230                DistributionId::RelativeUrl(base.to_string(), url.to_string())
1231            }
1232            Self::AbsoluteUrl(url) => DistributionId::AbsoluteUrl(url.to_string()),
1233        }
1234    }
1235
1236    fn resource_id(&self) -> ResourceId {
1237        match self {
1238            Self::RelativeUrl(base, url) => {
1239                ResourceId::RelativeUrl(base.to_string(), url.to_string())
1240            }
1241            Self::AbsoluteUrl(url) => ResourceId::AbsoluteUrl(url.to_string()),
1242        }
1243    }
1244}
1245
1246impl Identifier for RegistryBuiltWheel {
1247    fn distribution_id(&self) -> DistributionId {
1248        self.file.distribution_id()
1249    }
1250
1251    fn resource_id(&self) -> ResourceId {
1252        self.file.resource_id()
1253    }
1254}
1255
1256impl Identifier for RegistryBuiltDist {
1257    fn distribution_id(&self) -> DistributionId {
1258        self.best_wheel().distribution_id()
1259    }
1260
1261    fn resource_id(&self) -> ResourceId {
1262        self.best_wheel().resource_id()
1263    }
1264}
1265
1266impl Identifier for RegistrySourceDist {
1267    fn distribution_id(&self) -> DistributionId {
1268        self.file.distribution_id()
1269    }
1270
1271    fn resource_id(&self) -> ResourceId {
1272        self.file.resource_id()
1273    }
1274}
1275
1276impl Identifier for DirectUrlBuiltDist {
1277    fn distribution_id(&self) -> DistributionId {
1278        self.url.distribution_id()
1279    }
1280
1281    fn resource_id(&self) -> ResourceId {
1282        self.url.resource_id()
1283    }
1284}
1285
1286impl Identifier for DirectUrlSourceDist {
1287    fn distribution_id(&self) -> DistributionId {
1288        self.url.distribution_id()
1289    }
1290
1291    fn resource_id(&self) -> ResourceId {
1292        self.url.resource_id()
1293    }
1294}
1295
1296impl Identifier for PathBuiltDist {
1297    fn distribution_id(&self) -> DistributionId {
1298        self.url.distribution_id()
1299    }
1300
1301    fn resource_id(&self) -> ResourceId {
1302        self.url.resource_id()
1303    }
1304}
1305
1306impl Identifier for PathSourceDist {
1307    fn distribution_id(&self) -> DistributionId {
1308        self.url.distribution_id()
1309    }
1310
1311    fn resource_id(&self) -> ResourceId {
1312        self.url.resource_id()
1313    }
1314}
1315
1316impl Identifier for DirectorySourceDist {
1317    fn distribution_id(&self) -> DistributionId {
1318        self.url.distribution_id()
1319    }
1320
1321    fn resource_id(&self) -> ResourceId {
1322        self.url.resource_id()
1323    }
1324}
1325
1326impl Identifier for GitSourceDist {
1327    fn distribution_id(&self) -> DistributionId {
1328        self.url.distribution_id()
1329    }
1330
1331    fn resource_id(&self) -> ResourceId {
1332        self.url.resource_id()
1333    }
1334}
1335
1336impl Identifier for SourceDist {
1337    fn distribution_id(&self) -> DistributionId {
1338        match self {
1339            Self::Registry(dist) => dist.distribution_id(),
1340            Self::DirectUrl(dist) => dist.distribution_id(),
1341            Self::Git(dist) => dist.distribution_id(),
1342            Self::Path(dist) => dist.distribution_id(),
1343            Self::Directory(dist) => dist.distribution_id(),
1344        }
1345    }
1346
1347    fn resource_id(&self) -> ResourceId {
1348        match self {
1349            Self::Registry(dist) => dist.resource_id(),
1350            Self::DirectUrl(dist) => dist.resource_id(),
1351            Self::Git(dist) => dist.resource_id(),
1352            Self::Path(dist) => dist.resource_id(),
1353            Self::Directory(dist) => dist.resource_id(),
1354        }
1355    }
1356}
1357
1358impl Identifier for BuiltDist {
1359    fn distribution_id(&self) -> DistributionId {
1360        match self {
1361            Self::Registry(dist) => dist.distribution_id(),
1362            Self::DirectUrl(dist) => dist.distribution_id(),
1363            Self::Path(dist) => dist.distribution_id(),
1364        }
1365    }
1366
1367    fn resource_id(&self) -> ResourceId {
1368        match self {
1369            Self::Registry(dist) => dist.resource_id(),
1370            Self::DirectUrl(dist) => dist.resource_id(),
1371            Self::Path(dist) => dist.resource_id(),
1372        }
1373    }
1374}
1375
1376impl Identifier for InstalledDist {
1377    fn distribution_id(&self) -> DistributionId {
1378        self.install_path().distribution_id()
1379    }
1380
1381    fn resource_id(&self) -> ResourceId {
1382        self.install_path().resource_id()
1383    }
1384}
1385
1386impl Identifier for Dist {
1387    fn distribution_id(&self) -> DistributionId {
1388        match self {
1389            Self::Built(dist) => dist.distribution_id(),
1390            Self::Source(dist) => dist.distribution_id(),
1391        }
1392    }
1393
1394    fn resource_id(&self) -> ResourceId {
1395        match self {
1396            Self::Built(dist) => dist.resource_id(),
1397            Self::Source(dist) => dist.resource_id(),
1398        }
1399    }
1400}
1401
1402impl Identifier for DirectSourceUrl<'_> {
1403    fn distribution_id(&self) -> DistributionId {
1404        self.url.distribution_id()
1405    }
1406
1407    fn resource_id(&self) -> ResourceId {
1408        self.url.resource_id()
1409    }
1410}
1411
1412impl Identifier for GitSourceUrl<'_> {
1413    fn distribution_id(&self) -> DistributionId {
1414        self.url.distribution_id()
1415    }
1416
1417    fn resource_id(&self) -> ResourceId {
1418        self.url.resource_id()
1419    }
1420}
1421
1422impl Identifier for PathSourceUrl<'_> {
1423    fn distribution_id(&self) -> DistributionId {
1424        self.url.distribution_id()
1425    }
1426
1427    fn resource_id(&self) -> ResourceId {
1428        self.url.resource_id()
1429    }
1430}
1431
1432impl Identifier for DirectorySourceUrl<'_> {
1433    fn distribution_id(&self) -> DistributionId {
1434        self.url.distribution_id()
1435    }
1436
1437    fn resource_id(&self) -> ResourceId {
1438        self.url.resource_id()
1439    }
1440}
1441
1442impl Identifier for SourceUrl<'_> {
1443    fn distribution_id(&self) -> DistributionId {
1444        match self {
1445            Self::Direct(url) => url.distribution_id(),
1446            Self::Git(url) => url.distribution_id(),
1447            Self::Path(url) => url.distribution_id(),
1448            Self::Directory(url) => url.distribution_id(),
1449        }
1450    }
1451
1452    fn resource_id(&self) -> ResourceId {
1453        match self {
1454            Self::Direct(url) => url.resource_id(),
1455            Self::Git(url) => url.resource_id(),
1456            Self::Path(url) => url.resource_id(),
1457            Self::Directory(url) => url.resource_id(),
1458        }
1459    }
1460}
1461
1462impl Identifier for BuildableSource<'_> {
1463    fn distribution_id(&self) -> DistributionId {
1464        match self {
1465            Self::Dist(source) => source.distribution_id(),
1466            Self::Url(source) => source.distribution_id(),
1467        }
1468    }
1469
1470    fn resource_id(&self) -> ResourceId {
1471        match self {
1472            Self::Dist(source) => source.resource_id(),
1473            Self::Url(source) => source.resource_id(),
1474        }
1475    }
1476}
1477
1478#[cfg(test)]
1479mod test {
1480    use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString};
1481    use uv_redacted::DisplaySafeUrl;
1482
1483    /// Ensure that we don't accidentally grow the `Dist` sizes.
1484    #[test]
1485    fn dist_size() {
1486        assert!(size_of::<Dist>() <= 200, "{}", size_of::<Dist>());
1487        assert!(size_of::<BuiltDist>() <= 200, "{}", size_of::<BuiltDist>());
1488        assert!(
1489            size_of::<SourceDist>() <= 176,
1490            "{}",
1491            size_of::<SourceDist>()
1492        );
1493    }
1494
1495    #[test]
1496    fn remote_source() {
1497        for url in [
1498            "https://example.com/foo-0.1.0.tar.gz",
1499            "https://example.com/foo-0.1.0.tar.gz#fragment",
1500            "https://example.com/foo-0.1.0.tar.gz?query",
1501            "https://example.com/foo-0.1.0.tar.gz?query#fragment",
1502            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment",
1503            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3",
1504        ] {
1505            let url = DisplaySafeUrl::parse(url).unwrap();
1506            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1507            let url = UrlString::from(url.clone());
1508            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1509        }
1510    }
1511}