mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-11-04 05:34:28 +00:00 
			
		
		
		
	Enforce extension validity at parse time (#5888)
## Summary This PR adds a `DistExtension` field to some of our distribution types, which requires that we validate that the file type is known and supported when parsing (rather than when attempting to unzip). It removes a bunch of extension parsing from the code too, in favor of doing it once upfront. Closes https://github.com/astral-sh/uv/issues/5858.
This commit is contained in:
		
							parent
							
								
									ba7c09edd0
								
							
						
					
					
						commit
						21408c1f35
					
				
					 36 changed files with 803 additions and 480 deletions
				
			
		
							
								
								
									
										3
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -2812,6 +2812,7 @@ version = "0.0.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "anyhow",
 | 
					 "anyhow",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "distribution-filename",
 | 
				
			||||||
 "indexmap",
 | 
					 "indexmap",
 | 
				
			||||||
 "itertools 0.13.0",
 | 
					 "itertools 0.13.0",
 | 
				
			||||||
 "mailparse",
 | 
					 "mailparse",
 | 
				
			||||||
| 
						 | 
					@ -4816,6 +4817,7 @@ version = "0.0.1"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "async-compression",
 | 
					 "async-compression",
 | 
				
			||||||
 "async_zip",
 | 
					 "async_zip",
 | 
				
			||||||
 | 
					 "distribution-filename",
 | 
				
			||||||
 "fs-err",
 | 
					 "fs-err",
 | 
				
			||||||
 "futures",
 | 
					 "futures",
 | 
				
			||||||
 "md-5",
 | 
					 "md-5",
 | 
				
			||||||
| 
						 | 
					@ -4945,6 +4947,7 @@ dependencies = [
 | 
				
			||||||
 "cache-key",
 | 
					 "cache-key",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "configparser",
 | 
					 "configparser",
 | 
				
			||||||
 | 
					 "distribution-filename",
 | 
				
			||||||
 "fs-err",
 | 
					 "fs-err",
 | 
				
			||||||
 "futures",
 | 
					 "futures",
 | 
				
			||||||
 "indoc",
 | 
					 "indoc",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										99
									
								
								crates/distribution-filename/src/extension.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								crates/distribution-filename/src/extension.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,99 @@
 | 
				
			||||||
 | 
					use std::fmt::{Display, Formatter};
 | 
				
			||||||
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 | 
				
			||||||
 | 
					pub enum DistExtension {
 | 
				
			||||||
 | 
					    Wheel,
 | 
				
			||||||
 | 
					    Source(SourceDistExtension),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(
 | 
				
			||||||
 | 
					    Clone,
 | 
				
			||||||
 | 
					    Copy,
 | 
				
			||||||
 | 
					    Debug,
 | 
				
			||||||
 | 
					    PartialEq,
 | 
				
			||||||
 | 
					    Eq,
 | 
				
			||||||
 | 
					    PartialOrd,
 | 
				
			||||||
 | 
					    Ord,
 | 
				
			||||||
 | 
					    Hash,
 | 
				
			||||||
 | 
					    Serialize,
 | 
				
			||||||
 | 
					    Deserialize,
 | 
				
			||||||
 | 
					    rkyv::Archive,
 | 
				
			||||||
 | 
					    rkyv::Deserialize,
 | 
				
			||||||
 | 
					    rkyv::Serialize,
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					#[archive(check_bytes)]
 | 
				
			||||||
 | 
					#[archive_attr(derive(Debug))]
 | 
				
			||||||
 | 
					pub enum SourceDistExtension {
 | 
				
			||||||
 | 
					    Zip,
 | 
				
			||||||
 | 
					    TarGz,
 | 
				
			||||||
 | 
					    TarBz2,
 | 
				
			||||||
 | 
					    TarXz,
 | 
				
			||||||
 | 
					    TarZst,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl DistExtension {
 | 
				
			||||||
 | 
					    /// Extract the [`DistExtension`] from a path.
 | 
				
			||||||
 | 
					    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, ExtensionError> {
 | 
				
			||||||
 | 
					        let Some(extension) = path.as_ref().extension().and_then(|ext| ext.to_str()) else {
 | 
				
			||||||
 | 
					            return Err(ExtensionError::Dist);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match extension {
 | 
				
			||||||
 | 
					            "whl" => Ok(Self::Wheel),
 | 
				
			||||||
 | 
					            _ => SourceDistExtension::from_path(path)
 | 
				
			||||||
 | 
					                .map(Self::Source)
 | 
				
			||||||
 | 
					                .map_err(|_| ExtensionError::Dist),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SourceDistExtension {
 | 
				
			||||||
 | 
					    /// Extract the [`SourceDistExtension`] from a path.
 | 
				
			||||||
 | 
					    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, ExtensionError> {
 | 
				
			||||||
 | 
					        /// Returns true if the path is a tar file (e.g., `.tar.gz`).
 | 
				
			||||||
 | 
					        fn is_tar(path: &Path) -> bool {
 | 
				
			||||||
 | 
					            path.file_stem().is_some_and(|stem| {
 | 
				
			||||||
 | 
					                Path::new(stem)
 | 
				
			||||||
 | 
					                    .extension()
 | 
				
			||||||
 | 
					                    .is_some_and(|ext| ext.eq_ignore_ascii_case("tar"))
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let Some(extension) = path.as_ref().extension().and_then(|ext| ext.to_str()) else {
 | 
				
			||||||
 | 
					            return Err(ExtensionError::SourceDist);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match extension {
 | 
				
			||||||
 | 
					            "zip" => Ok(Self::Zip),
 | 
				
			||||||
 | 
					            "gz" if is_tar(path.as_ref()) => Ok(Self::TarGz),
 | 
				
			||||||
 | 
					            "bz2" if is_tar(path.as_ref()) => Ok(Self::TarBz2),
 | 
				
			||||||
 | 
					            "xz" if is_tar(path.as_ref()) => Ok(Self::TarXz),
 | 
				
			||||||
 | 
					            "zst" if is_tar(path.as_ref()) => Ok(Self::TarZst),
 | 
				
			||||||
 | 
					            _ => Err(ExtensionError::SourceDist),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for SourceDistExtension {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Zip => f.write_str("zip"),
 | 
				
			||||||
 | 
					            Self::TarGz => f.write_str("tar.gz"),
 | 
				
			||||||
 | 
					            Self::TarBz2 => f.write_str("tar.bz2"),
 | 
				
			||||||
 | 
					            Self::TarXz => f.write_str("tar.xz"),
 | 
				
			||||||
 | 
					            Self::TarZst => f.write_str("tar.zst"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Error, Debug)]
 | 
				
			||||||
 | 
					pub enum ExtensionError {
 | 
				
			||||||
 | 
					    #[error("`.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`")]
 | 
				
			||||||
 | 
					    Dist,
 | 
				
			||||||
 | 
					    #[error("`.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`")]
 | 
				
			||||||
 | 
					    SourceDist,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,11 +5,13 @@ use uv_normalize::PackageName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use build_tag::{BuildTag, BuildTagError};
 | 
					pub use build_tag::{BuildTag, BuildTagError};
 | 
				
			||||||
pub use egg::{EggInfoFilename, EggInfoFilenameError};
 | 
					pub use egg::{EggInfoFilename, EggInfoFilenameError};
 | 
				
			||||||
pub use source_dist::{SourceDistExtension, SourceDistFilename, SourceDistFilenameError};
 | 
					pub use extension::{DistExtension, ExtensionError, SourceDistExtension};
 | 
				
			||||||
 | 
					pub use source_dist::SourceDistFilename;
 | 
				
			||||||
pub use wheel::{WheelFilename, WheelFilenameError};
 | 
					pub use wheel::{WheelFilename, WheelFilenameError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod build_tag;
 | 
					mod build_tag;
 | 
				
			||||||
mod egg;
 | 
					mod egg;
 | 
				
			||||||
 | 
					mod extension;
 | 
				
			||||||
mod source_dist;
 | 
					mod source_dist;
 | 
				
			||||||
mod wheel;
 | 
					mod wheel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,14 +24,21 @@ pub enum DistFilename {
 | 
				
			||||||
impl DistFilename {
 | 
					impl DistFilename {
 | 
				
			||||||
    /// Parse a filename as wheel or source dist name.
 | 
					    /// Parse a filename as wheel or source dist name.
 | 
				
			||||||
    pub fn try_from_filename(filename: &str, package_name: &PackageName) -> Option<Self> {
 | 
					    pub fn try_from_filename(filename: &str, package_name: &PackageName) -> Option<Self> {
 | 
				
			||||||
 | 
					        match DistExtension::from_path(filename) {
 | 
				
			||||||
 | 
					            Ok(DistExtension::Wheel) => {
 | 
				
			||||||
                if let Ok(filename) = WheelFilename::from_str(filename) {
 | 
					                if let Ok(filename) = WheelFilename::from_str(filename) {
 | 
				
			||||||
            Some(Self::WheelFilename(filename))
 | 
					                    return Some(Self::WheelFilename(filename));
 | 
				
			||||||
        } else if let Ok(filename) = SourceDistFilename::parse(filename, package_name) {
 | 
					 | 
				
			||||||
            Some(Self::SourceDistFilename(filename))
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(DistExtension::Source(extension)) => {
 | 
				
			||||||
 | 
					                if let Ok(filename) = SourceDistFilename::parse(filename, extension, package_name) {
 | 
				
			||||||
 | 
					                    return Some(Self::SourceDistFilename(filename));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(_) => {}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Like [`DistFilename::try_from_normalized_filename`], but without knowing the package name.
 | 
					    /// Like [`DistFilename::try_from_normalized_filename`], but without knowing the package name.
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,69 +1,12 @@
 | 
				
			||||||
use std::fmt::{Display, Formatter};
 | 
					use std::fmt::{Display, Formatter};
 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::SourceDistExtension;
 | 
				
			||||||
 | 
					use pep440_rs::{Version, VersionParseError};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use thiserror::Error;
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use pep440_rs::{Version, VersionParseError};
 | 
					 | 
				
			||||||
use uv_normalize::{InvalidNameError, PackageName};
 | 
					use uv_normalize::{InvalidNameError, PackageName};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(
 | 
					 | 
				
			||||||
    Clone,
 | 
					 | 
				
			||||||
    Debug,
 | 
					 | 
				
			||||||
    PartialEq,
 | 
					 | 
				
			||||||
    Eq,
 | 
					 | 
				
			||||||
    Serialize,
 | 
					 | 
				
			||||||
    Deserialize,
 | 
					 | 
				
			||||||
    rkyv::Archive,
 | 
					 | 
				
			||||||
    rkyv::Deserialize,
 | 
					 | 
				
			||||||
    rkyv::Serialize,
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
#[archive(check_bytes)]
 | 
					 | 
				
			||||||
#[archive_attr(derive(Debug))]
 | 
					 | 
				
			||||||
pub enum SourceDistExtension {
 | 
					 | 
				
			||||||
    Zip,
 | 
					 | 
				
			||||||
    TarGz,
 | 
					 | 
				
			||||||
    TarBz2,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl FromStr for SourceDistExtension {
 | 
					 | 
				
			||||||
    type Err = String;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn from_str(s: &str) -> Result<Self, Self::Err> {
 | 
					 | 
				
			||||||
        Ok(match s {
 | 
					 | 
				
			||||||
            "zip" => Self::Zip,
 | 
					 | 
				
			||||||
            "tar.gz" => Self::TarGz,
 | 
					 | 
				
			||||||
            "tar.bz2" => Self::TarBz2,
 | 
					 | 
				
			||||||
            other => return Err(other.to_string()),
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Display for SourceDistExtension {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::Zip => f.write_str("zip"),
 | 
					 | 
				
			||||||
            Self::TarGz => f.write_str("tar.gz"),
 | 
					 | 
				
			||||||
            Self::TarBz2 => f.write_str("tar.bz2"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SourceDistExtension {
 | 
					 | 
				
			||||||
    pub fn from_filename(filename: &str) -> Option<(&str, Self)> {
 | 
					 | 
				
			||||||
        if let Some(stem) = filename.strip_suffix(".zip") {
 | 
					 | 
				
			||||||
            return Some((stem, Self::Zip));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let Some(stem) = filename.strip_suffix(".tar.gz") {
 | 
					 | 
				
			||||||
            return Some((stem, Self::TarGz));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if let Some(stem) = filename.strip_suffix(".tar.bz2") {
 | 
					 | 
				
			||||||
            return Some((stem, Self::TarBz2));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        None
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Note that this is a normalized and not an exact representation, keep the original string if you
 | 
					/// Note that this is a normalized and not an exact representation, keep the original string if you
 | 
				
			||||||
/// need the latter.
 | 
					/// need the latter.
 | 
				
			||||||
#[derive(
 | 
					#[derive(
 | 
				
			||||||
| 
						 | 
					@ -90,14 +33,18 @@ impl SourceDistFilename {
 | 
				
			||||||
    /// these (consider e.g. `a-1-1.zip`)
 | 
					    /// these (consider e.g. `a-1-1.zip`)
 | 
				
			||||||
    pub fn parse(
 | 
					    pub fn parse(
 | 
				
			||||||
        filename: &str,
 | 
					        filename: &str,
 | 
				
			||||||
 | 
					        extension: SourceDistExtension,
 | 
				
			||||||
        package_name: &PackageName,
 | 
					        package_name: &PackageName,
 | 
				
			||||||
    ) -> Result<Self, SourceDistFilenameError> {
 | 
					    ) -> Result<Self, SourceDistFilenameError> {
 | 
				
			||||||
        let Some((stem, extension)) = SourceDistExtension::from_filename(filename) else {
 | 
					        // Drop the extension (e.g., given `tar.gz`, drop `.tar.gz`).
 | 
				
			||||||
 | 
					        if filename.len() <= extension.to_string().len() + 1 {
 | 
				
			||||||
            return Err(SourceDistFilenameError {
 | 
					            return Err(SourceDistFilenameError {
 | 
				
			||||||
                filename: filename.to_string(),
 | 
					                filename: filename.to_string(),
 | 
				
			||||||
                kind: SourceDistFilenameErrorKind::Extension,
 | 
					                kind: SourceDistFilenameErrorKind::Extension,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        };
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let stem = &filename[..(filename.len() - (extension.to_string().len() + 1))];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if stem.len() <= package_name.as_ref().len() + "-".len() {
 | 
					        if stem.len() <= package_name.as_ref().len() + "-".len() {
 | 
				
			||||||
            return Err(SourceDistFilenameError {
 | 
					            return Err(SourceDistFilenameError {
 | 
				
			||||||
| 
						 | 
					@ -138,13 +85,23 @@ impl SourceDistFilename {
 | 
				
			||||||
    /// Source dist filenames can be ambiguous, e.g. `a-1-1.tar.gz`. Without knowing the package name, we assume that
 | 
					    /// Source dist filenames can be ambiguous, e.g. `a-1-1.tar.gz`. Without knowing the package name, we assume that
 | 
				
			||||||
    /// source dist filename version doesn't contain minus (the version is normalized).
 | 
					    /// source dist filename version doesn't contain minus (the version is normalized).
 | 
				
			||||||
    pub fn parsed_normalized_filename(filename: &str) -> Result<Self, SourceDistFilenameError> {
 | 
					    pub fn parsed_normalized_filename(filename: &str) -> Result<Self, SourceDistFilenameError> {
 | 
				
			||||||
        let Some((stem, extension)) = SourceDistExtension::from_filename(filename) else {
 | 
					        let Ok(extension) = SourceDistExtension::from_path(filename) else {
 | 
				
			||||||
            return Err(SourceDistFilenameError {
 | 
					            return Err(SourceDistFilenameError {
 | 
				
			||||||
                filename: filename.to_string(),
 | 
					                filename: filename.to_string(),
 | 
				
			||||||
                kind: SourceDistFilenameErrorKind::Extension,
 | 
					                kind: SourceDistFilenameErrorKind::Extension,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Drop the extension (e.g., given `tar.gz`, drop `.tar.gz`).
 | 
				
			||||||
 | 
					        if filename.len() <= extension.to_string().len() + 1 {
 | 
				
			||||||
 | 
					            return Err(SourceDistFilenameError {
 | 
				
			||||||
 | 
					                filename: filename.to_string(),
 | 
				
			||||||
 | 
					                kind: SourceDistFilenameErrorKind::Extension,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let stem = &filename[..(filename.len() - (extension.to_string().len() + 1))];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let Some((package_name, version)) = stem.rsplit_once('-') else {
 | 
					        let Some((package_name, version)) = stem.rsplit_once('-') else {
 | 
				
			||||||
            return Err(SourceDistFilenameError {
 | 
					            return Err(SourceDistFilenameError {
 | 
				
			||||||
                filename: filename.to_string(),
 | 
					                filename: filename.to_string(),
 | 
				
			||||||
| 
						 | 
					@ -197,7 +154,7 @@ impl Display for SourceDistFilenameError {
 | 
				
			||||||
enum SourceDistFilenameErrorKind {
 | 
					enum SourceDistFilenameErrorKind {
 | 
				
			||||||
    #[error("Name doesn't start with package name {0}")]
 | 
					    #[error("Name doesn't start with package name {0}")]
 | 
				
			||||||
    Filename(PackageName),
 | 
					    Filename(PackageName),
 | 
				
			||||||
    #[error("Source distributions filenames must end with .zip, .tar.gz, or .tar.bz2")]
 | 
					    #[error("File extension is invalid")]
 | 
				
			||||||
    Extension,
 | 
					    Extension,
 | 
				
			||||||
    #[error("Version section is invalid")]
 | 
					    #[error("Version section is invalid")]
 | 
				
			||||||
    Version(#[from] VersionParseError),
 | 
					    Version(#[from] VersionParseError),
 | 
				
			||||||
| 
						 | 
					@ -213,7 +170,7 @@ mod tests {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use uv_normalize::PackageName;
 | 
					    use uv_normalize::PackageName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use crate::SourceDistFilename;
 | 
					    use crate::{SourceDistExtension, SourceDistFilename};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Only test already normalized names since the parsing is lossy
 | 
					    /// Only test already normalized names since the parsing is lossy
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
| 
						 | 
					@ -223,9 +180,15 @@ mod tests {
 | 
				
			||||||
            "foo-lib-1.2.3a3.zip",
 | 
					            "foo-lib-1.2.3a3.zip",
 | 
				
			||||||
            "foo-lib-1.2.3.tar.gz",
 | 
					            "foo-lib-1.2.3.tar.gz",
 | 
				
			||||||
            "foo-lib-1.2.3.tar.bz2",
 | 
					            "foo-lib-1.2.3.tar.bz2",
 | 
				
			||||||
 | 
					            "foo-lib-1.2.3.tar.zst",
 | 
				
			||||||
        ] {
 | 
					        ] {
 | 
				
			||||||
 | 
					            let ext = SourceDistExtension::from_path(normalized).unwrap();
 | 
				
			||||||
            assert_eq!(
 | 
					            assert_eq!(
 | 
				
			||||||
                SourceDistFilename::parse(normalized, &PackageName::from_str("foo_lib").unwrap())
 | 
					                SourceDistFilename::parse(
 | 
				
			||||||
 | 
					                    normalized,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
 | 
					                    &PackageName::from_str("foo_lib").unwrap()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                .unwrap()
 | 
					                .unwrap()
 | 
				
			||||||
                .to_string(),
 | 
					                .to_string(),
 | 
				
			||||||
                normalized
 | 
					                normalized
 | 
				
			||||||
| 
						 | 
					@ -235,18 +198,22 @@ mod tests {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn errors() {
 | 
					    fn errors() {
 | 
				
			||||||
        for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip", "a-1.2.3.tar.zstd"] {
 | 
					        for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip"] {
 | 
				
			||||||
 | 
					            let ext = SourceDistExtension::from_path(invalid).unwrap();
 | 
				
			||||||
            assert!(
 | 
					            assert!(
 | 
				
			||||||
                SourceDistFilename::parse(invalid, &PackageName::from_str("a").unwrap()).is_err()
 | 
					                SourceDistFilename::parse(invalid, ext, &PackageName::from_str("a").unwrap())
 | 
				
			||||||
 | 
					                    .is_err()
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn name_to_long() {
 | 
					    fn name_too_long() {
 | 
				
			||||||
        assert!(
 | 
					        assert!(SourceDistFilename::parse(
 | 
				
			||||||
            SourceDistFilename::parse("foo.zip", &PackageName::from_str("foo-lib").unwrap())
 | 
					            "foo.zip",
 | 
				
			||||||
                .is_err()
 | 
					            SourceDistExtension::Zip,
 | 
				
			||||||
        );
 | 
					            &PackageName::from_str("foo-lib").unwrap()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .is_err());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
use std::borrow::Cow;
 | 
					use std::borrow::Cow;
 | 
				
			||||||
use std::path::Path;
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use distribution_filename::SourceDistExtension;
 | 
				
			||||||
use pep440_rs::Version;
 | 
					use pep440_rs::Version;
 | 
				
			||||||
use pep508_rs::VerbatimUrl;
 | 
					use pep508_rs::VerbatimUrl;
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
| 
						 | 
					@ -109,6 +110,8 @@ impl std::fmt::Display for SourceUrl<'_> {
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct DirectSourceUrl<'a> {
 | 
					pub struct DirectSourceUrl<'a> {
 | 
				
			||||||
    pub url: &'a Url,
 | 
					    pub url: &'a Url,
 | 
				
			||||||
 | 
					    pub subdirectory: Option<&'a Path>,
 | 
				
			||||||
 | 
					    pub ext: SourceDistExtension,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl std::fmt::Display for DirectSourceUrl<'_> {
 | 
					impl std::fmt::Display for DirectSourceUrl<'_> {
 | 
				
			||||||
| 
						 | 
					@ -146,6 +149,7 @@ impl<'a> From<&'a GitSourceDist> for GitSourceUrl<'a> {
 | 
				
			||||||
pub struct PathSourceUrl<'a> {
 | 
					pub struct PathSourceUrl<'a> {
 | 
				
			||||||
    pub url: &'a Url,
 | 
					    pub url: &'a Url,
 | 
				
			||||||
    pub path: Cow<'a, Path>,
 | 
					    pub path: Cow<'a, Path>,
 | 
				
			||||||
 | 
					    pub ext: SourceDistExtension,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl std::fmt::Display for PathSourceUrl<'_> {
 | 
					impl std::fmt::Display for PathSourceUrl<'_> {
 | 
				
			||||||
| 
						 | 
					@ -159,6 +163,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> {
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            url: &dist.url,
 | 
					            url: &dist.url,
 | 
				
			||||||
            path: Cow::Borrowed(&dist.install_path),
 | 
					            path: Cow::Borrowed(&dist.install_path),
 | 
				
			||||||
 | 
					            ext: dist.ext,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use distribution_filename::WheelFilename;
 | 
					use distribution_filename::{DistExtension, SourceDistExtension, WheelFilename};
 | 
				
			||||||
use pep440_rs::Version;
 | 
					use pep440_rs::Version;
 | 
				
			||||||
use pep508_rs::{Pep508Url, VerbatimUrl};
 | 
					use pep508_rs::{Pep508Url, VerbatimUrl};
 | 
				
			||||||
use pypi_types::{ParsedUrl, VerbatimParsedUrl};
 | 
					use pypi_types::{ParsedUrl, VerbatimParsedUrl};
 | 
				
			||||||
| 
						 | 
					@ -228,6 +228,8 @@ pub struct RegistrySourceDist {
 | 
				
			||||||
    pub name: PackageName,
 | 
					    pub name: PackageName,
 | 
				
			||||||
    pub version: Version,
 | 
					    pub version: Version,
 | 
				
			||||||
    pub file: Box<File>,
 | 
					    pub file: Box<File>,
 | 
				
			||||||
 | 
					    /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
 | 
					    pub ext: SourceDistExtension,
 | 
				
			||||||
    pub index: IndexUrl,
 | 
					    pub index: IndexUrl,
 | 
				
			||||||
    /// When an sdist is selected, it may be the case that there were
 | 
					    /// When an sdist is selected, it may be the case that there were
 | 
				
			||||||
    /// available wheels too. There are many reasons why a wheel might not
 | 
					    /// available wheels too. There are many reasons why a wheel might not
 | 
				
			||||||
| 
						 | 
					@ -249,6 +251,8 @@ pub struct DirectUrlSourceDist {
 | 
				
			||||||
    pub location: Url,
 | 
					    pub location: Url,
 | 
				
			||||||
    /// The subdirectory within the archive in which the source distribution is located.
 | 
					    /// The subdirectory within the archive in which the source distribution is located.
 | 
				
			||||||
    pub subdirectory: Option<PathBuf>,
 | 
					    pub subdirectory: Option<PathBuf>,
 | 
				
			||||||
 | 
					    /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
 | 
					    pub ext: SourceDistExtension,
 | 
				
			||||||
    /// The URL as it was provided by the user, including the subdirectory fragment.
 | 
					    /// The URL as it was provided by the user, including the subdirectory fragment.
 | 
				
			||||||
    pub url: VerbatimUrl,
 | 
					    pub url: VerbatimUrl,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -275,6 +279,8 @@ pub struct PathSourceDist {
 | 
				
			||||||
    /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
					    /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
				
			||||||
    /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
					    /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
				
			||||||
    pub lock_path: PathBuf,
 | 
					    pub lock_path: PathBuf,
 | 
				
			||||||
 | 
					    /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
 | 
					    pub ext: SourceDistExtension,
 | 
				
			||||||
    /// The URL as it was provided by the user.
 | 
					    /// The URL as it was provided by the user.
 | 
				
			||||||
    pub url: VerbatimUrl,
 | 
					    pub url: VerbatimUrl,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -303,11 +309,10 @@ impl Dist {
 | 
				
			||||||
        url: VerbatimUrl,
 | 
					        url: VerbatimUrl,
 | 
				
			||||||
        location: Url,
 | 
					        location: Url,
 | 
				
			||||||
        subdirectory: Option<PathBuf>,
 | 
					        subdirectory: Option<PathBuf>,
 | 
				
			||||||
 | 
					        ext: DistExtension,
 | 
				
			||||||
    ) -> Result<Dist, Error> {
 | 
					    ) -> Result<Dist, Error> {
 | 
				
			||||||
        if Path::new(url.path())
 | 
					        match ext {
 | 
				
			||||||
            .extension()
 | 
					            DistExtension::Wheel => {
 | 
				
			||||||
            .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                // Validate that the name in the wheel matches that of the requirement.
 | 
					                // Validate that the name in the wheel matches that of the requirement.
 | 
				
			||||||
                let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
					                let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
				
			||||||
                if filename.name != name {
 | 
					                if filename.name != name {
 | 
				
			||||||
| 
						 | 
					@ -323,15 +328,18 @@ impl Dist {
 | 
				
			||||||
                    location,
 | 
					                    location,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                })))
 | 
					                })))
 | 
				
			||||||
        } else {
 | 
					            }
 | 
				
			||||||
 | 
					            DistExtension::Source(ext) => {
 | 
				
			||||||
                Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
 | 
					                Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
 | 
				
			||||||
                    name,
 | 
					                    name,
 | 
				
			||||||
                    location,
 | 
					                    location,
 | 
				
			||||||
                    subdirectory,
 | 
					                    subdirectory,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                })))
 | 
					                })))
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// A local built or source distribution from a `file://` URL.
 | 
					    /// A local built or source distribution from a `file://` URL.
 | 
				
			||||||
    pub fn from_file_url(
 | 
					    pub fn from_file_url(
 | 
				
			||||||
| 
						 | 
					@ -339,6 +347,7 @@ impl Dist {
 | 
				
			||||||
        url: VerbatimUrl,
 | 
					        url: VerbatimUrl,
 | 
				
			||||||
        install_path: &Path,
 | 
					        install_path: &Path,
 | 
				
			||||||
        lock_path: &Path,
 | 
					        lock_path: &Path,
 | 
				
			||||||
 | 
					        ext: DistExtension,
 | 
				
			||||||
    ) -> Result<Dist, Error> {
 | 
					    ) -> Result<Dist, Error> {
 | 
				
			||||||
        // Store the canonicalized path, which also serves to validate that it exists.
 | 
					        // Store the canonicalized path, which also serves to validate that it exists.
 | 
				
			||||||
        let canonicalized_path = match install_path.canonicalize() {
 | 
					        let canonicalized_path = match install_path.canonicalize() {
 | 
				
			||||||
| 
						 | 
					@ -350,10 +359,8 @@ impl Dist {
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Determine whether the path represents a built or source distribution.
 | 
					        // Determine whether the path represents a built or source distribution.
 | 
				
			||||||
        if canonicalized_path
 | 
					        match ext {
 | 
				
			||||||
            .extension()
 | 
					            DistExtension::Wheel => {
 | 
				
			||||||
            .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
                // Validate that the name in the wheel matches that of the requirement.
 | 
					                // Validate that the name in the wheel matches that of the requirement.
 | 
				
			||||||
                let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
					                let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
				
			||||||
                if filename.name != name {
 | 
					                if filename.name != name {
 | 
				
			||||||
| 
						 | 
					@ -368,13 +375,14 @@ impl Dist {
 | 
				
			||||||
                    path: canonicalized_path,
 | 
					                    path: canonicalized_path,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                })))
 | 
					                })))
 | 
				
			||||||
        } else {
 | 
					            }
 | 
				
			||||||
            Ok(Self::Source(SourceDist::Path(PathSourceDist {
 | 
					            DistExtension::Source(ext) => Ok(Self::Source(SourceDist::Path(PathSourceDist {
 | 
				
			||||||
                name,
 | 
					                name,
 | 
				
			||||||
                install_path: canonicalized_path.clone(),
 | 
					                install_path: canonicalized_path.clone(),
 | 
				
			||||||
                lock_path: lock_path.to_path_buf(),
 | 
					                lock_path: lock_path.to_path_buf(),
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
            })))
 | 
					            }))),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -423,12 +431,20 @@ impl Dist {
 | 
				
			||||||
    /// Create a [`Dist`] for a URL-based distribution.
 | 
					    /// Create a [`Dist`] for a URL-based distribution.
 | 
				
			||||||
    pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result<Self, Error> {
 | 
					    pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result<Self, Error> {
 | 
				
			||||||
        match url.parsed_url {
 | 
					        match url.parsed_url {
 | 
				
			||||||
            ParsedUrl::Archive(archive) => {
 | 
					            ParsedUrl::Archive(archive) => Self::from_http_url(
 | 
				
			||||||
                Self::from_http_url(name, url.verbatim, archive.url, archive.subdirectory)
 | 
					                name,
 | 
				
			||||||
            }
 | 
					                url.verbatim,
 | 
				
			||||||
            ParsedUrl::Path(file) => {
 | 
					                archive.url,
 | 
				
			||||||
                Self::from_file_url(name, url.verbatim, &file.install_path, &file.lock_path)
 | 
					                archive.subdirectory,
 | 
				
			||||||
            }
 | 
					                archive.ext,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            ParsedUrl::Path(file) => Self::from_file_url(
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                url.verbatim,
 | 
				
			||||||
 | 
					                &file.install_path,
 | 
				
			||||||
 | 
					                &file.lock_path,
 | 
				
			||||||
 | 
					                file.ext,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            ParsedUrl::Directory(directory) => Self::from_directory_url(
 | 
					            ParsedUrl::Directory(directory) => Self::from_directory_url(
 | 
				
			||||||
                name,
 | 
					                name,
 | 
				
			||||||
                url.verbatim,
 | 
					                url.verbatim,
 | 
				
			||||||
| 
						 | 
					@ -1262,7 +1278,7 @@ mod test {
 | 
				
			||||||
            std::mem::size_of::<BuiltDist>()
 | 
					            std::mem::size_of::<BuiltDist>()
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        assert!(
 | 
					        assert!(
 | 
				
			||||||
            std::mem::size_of::<SourceDist>() <= 256,
 | 
					            std::mem::size_of::<SourceDist>() <= 264,
 | 
				
			||||||
            "{}",
 | 
					            "{}",
 | 
				
			||||||
            std::mem::size_of::<SourceDist>()
 | 
					            std::mem::size_of::<SourceDist>()
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
use std::collections::BTreeMap;
 | 
					use distribution_filename::DistExtension;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use pypi_types::{HashDigest, Requirement, RequirementSource};
 | 
					use pypi_types::{HashDigest, Requirement, RequirementSource};
 | 
				
			||||||
 | 
					use std::collections::BTreeMap;
 | 
				
			||||||
use uv_normalize::{ExtraName, GroupName, PackageName};
 | 
					use uv_normalize::{ExtraName, GroupName, PackageName};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist};
 | 
					use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist};
 | 
				
			||||||
| 
						 | 
					@ -143,12 +143,14 @@ impl From<&ResolvedDist> for Requirement {
 | 
				
			||||||
                        url: wheel.url.clone(),
 | 
					                        url: wheel.url.clone(),
 | 
				
			||||||
                        location,
 | 
					                        location,
 | 
				
			||||||
                        subdirectory: None,
 | 
					                        subdirectory: None,
 | 
				
			||||||
 | 
					                        ext: DistExtension::Wheel,
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Dist::Built(BuiltDist::Path(wheel)) => RequirementSource::Path {
 | 
					                Dist::Built(BuiltDist::Path(wheel)) => RequirementSource::Path {
 | 
				
			||||||
                    install_path: wheel.path.clone(),
 | 
					                    install_path: wheel.path.clone(),
 | 
				
			||||||
                    lock_path: wheel.path.clone(),
 | 
					                    lock_path: wheel.path.clone(),
 | 
				
			||||||
                    url: wheel.url.clone(),
 | 
					                    url: wheel.url.clone(),
 | 
				
			||||||
 | 
					                    ext: DistExtension::Wheel,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                Dist::Source(SourceDist::Registry(sdist)) => RequirementSource::Registry {
 | 
					                Dist::Source(SourceDist::Registry(sdist)) => RequirementSource::Registry {
 | 
				
			||||||
                    specifier: pep440_rs::VersionSpecifiers::from(
 | 
					                    specifier: pep440_rs::VersionSpecifiers::from(
 | 
				
			||||||
| 
						 | 
					@ -163,6 +165,7 @@ impl From<&ResolvedDist> for Requirement {
 | 
				
			||||||
                        url: sdist.url.clone(),
 | 
					                        url: sdist.url.clone(),
 | 
				
			||||||
                        location,
 | 
					                        location,
 | 
				
			||||||
                        subdirectory: sdist.subdirectory.clone(),
 | 
					                        subdirectory: sdist.subdirectory.clone(),
 | 
				
			||||||
 | 
					                        ext: DistExtension::Source(sdist.ext),
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                Dist::Source(SourceDist::Git(sdist)) => RequirementSource::Git {
 | 
					                Dist::Source(SourceDist::Git(sdist)) => RequirementSource::Git {
 | 
				
			||||||
| 
						 | 
					@ -176,6 +179,7 @@ impl From<&ResolvedDist> for Requirement {
 | 
				
			||||||
                    install_path: sdist.install_path.clone(),
 | 
					                    install_path: sdist.install_path.clone(),
 | 
				
			||||||
                    lock_path: sdist.lock_path.clone(),
 | 
					                    lock_path: sdist.lock_path.clone(),
 | 
				
			||||||
                    url: sdist.url.clone(),
 | 
					                    url: sdist.url.clone(),
 | 
				
			||||||
 | 
					                    ext: DistExtension::Source(sdist.ext),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Directory {
 | 
					                Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Directory {
 | 
				
			||||||
                    install_path: sdist.install_path.clone(),
 | 
					                    install_path: sdist.install_path.clone(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -693,6 +693,11 @@ fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Ex) `foo/bar`
 | 
				
			||||||
 | 
					    if expanded.contains('/') || expanded.contains('\\') {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    false
 | 
					    false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1010,7 +1015,7 @@ fn parse_pep508_requirement<T: Pep508Url>(
 | 
				
			||||||
            // a package name. pip supports this in `requirements.txt`, but it doesn't adhere to
 | 
					            // a package name. pip supports this in `requirements.txt`, but it doesn't adhere to
 | 
				
			||||||
            // the PEP 508 grammar.
 | 
					            // the PEP 508 grammar.
 | 
				
			||||||
            let mut clone = cursor.clone().at(start);
 | 
					            let mut clone = cursor.clone().at(start);
 | 
				
			||||||
            return if parse_url::<T>(&mut clone, working_dir).is_ok() {
 | 
					            return if looks_like_unnamed_requirement(&mut clone) {
 | 
				
			||||||
                Err(Pep508Error {
 | 
					                Err(Pep508Error {
 | 
				
			||||||
                    message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).".to_string()),
 | 
					                    message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).".to_string()),
 | 
				
			||||||
                    start,
 | 
					                    start,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ license = { workspace = true }
 | 
				
			||||||
workspace = true
 | 
					workspace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					distribution-filename = { workspace = true }
 | 
				
			||||||
pep440_rs = { workspace = true }
 | 
					pep440_rs = { workspace = true }
 | 
				
			||||||
pep508_rs = { workspace = true }
 | 
					pep508_rs = { workspace = true }
 | 
				
			||||||
uv-fs = { workspace = true, features = ["serde"] }
 | 
					uv-fs = { workspace = true, features = ["serde"] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
 | 
					use distribution_filename::{DistExtension, ExtensionError};
 | 
				
			||||||
 | 
					use pep508_rs::{Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError};
 | 
				
			||||||
use std::fmt::{Display, Formatter};
 | 
					use std::fmt::{Display, Formatter};
 | 
				
			||||||
use std::path::{Path, PathBuf};
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
use thiserror::Error;
 | 
					use thiserror::Error;
 | 
				
			||||||
use url::{ParseError, Url};
 | 
					use url::{ParseError, Url};
 | 
				
			||||||
 | 
					 | 
				
			||||||
use pep508_rs::{Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError};
 | 
					 | 
				
			||||||
use uv_git::{GitReference, GitSha, GitUrl, OidParseError};
 | 
					use uv_git::{GitReference, GitSha, GitUrl, OidParseError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind};
 | 
					use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind};
 | 
				
			||||||
| 
						 | 
					@ -13,17 +13,21 @@ pub enum ParsedUrlError {
 | 
				
			||||||
    #[error("Unsupported URL prefix `{prefix}` in URL: `{url}` ({message})")]
 | 
					    #[error("Unsupported URL prefix `{prefix}` in URL: `{url}` ({message})")]
 | 
				
			||||||
    UnsupportedUrlPrefix {
 | 
					    UnsupportedUrlPrefix {
 | 
				
			||||||
        prefix: String,
 | 
					        prefix: String,
 | 
				
			||||||
        url: Url,
 | 
					        url: String,
 | 
				
			||||||
        message: &'static str,
 | 
					        message: &'static str,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    #[error("Invalid path in file URL: `{0}`")]
 | 
					    #[error("Invalid path in file URL: `{0}`")]
 | 
				
			||||||
    InvalidFileUrl(Url),
 | 
					    InvalidFileUrl(String),
 | 
				
			||||||
    #[error("Failed to parse Git reference from URL: `{0}`")]
 | 
					    #[error("Failed to parse Git reference from URL: `{0}`")]
 | 
				
			||||||
    GitShaParse(Url, #[source] OidParseError),
 | 
					    GitShaParse(String, #[source] OidParseError),
 | 
				
			||||||
    #[error("Not a valid URL: `{0}`")]
 | 
					    #[error("Not a valid URL: `{0}`")]
 | 
				
			||||||
    UrlParse(String, #[source] ParseError),
 | 
					    UrlParse(String, #[source] ParseError),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
    VerbatimUrl(#[from] VerbatimUrlError),
 | 
					    VerbatimUrl(#[from] VerbatimUrlError),
 | 
				
			||||||
 | 
					    #[error("Expected direct URL (`{0}`) to end in a supported file extension: {1}")]
 | 
				
			||||||
 | 
					    MissingExtensionUrl(String, ExtensionError),
 | 
				
			||||||
 | 
					    #[error("Expected path (`{0}`) to end in a supported file extension: {1}")]
 | 
				
			||||||
 | 
					    MissingExtensionPath(PathBuf, ExtensionError),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)]
 | 
					#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)]
 | 
				
			||||||
| 
						 | 
					@ -75,6 +79,9 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
 | 
				
			||||||
                url: verbatim.to_url(),
 | 
					                url: verbatim.to_url(),
 | 
				
			||||||
                install_path: verbatim.as_path()?,
 | 
					                install_path: verbatim.as_path()?,
 | 
				
			||||||
                lock_path: path.as_ref().to_path_buf(),
 | 
					                lock_path: path.as_ref().to_path_buf(),
 | 
				
			||||||
 | 
					                ext: DistExtension::from_path(&path).map_err(|err| {
 | 
				
			||||||
 | 
					                    ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err)
 | 
				
			||||||
 | 
					                })?,
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        Ok(Self {
 | 
					        Ok(Self {
 | 
				
			||||||
| 
						 | 
					@ -103,6 +110,9 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
 | 
				
			||||||
                url: verbatim.to_url(),
 | 
					                url: verbatim.to_url(),
 | 
				
			||||||
                install_path: verbatim.as_path()?,
 | 
					                install_path: verbatim.as_path()?,
 | 
				
			||||||
                lock_path: path.as_ref().to_path_buf(),
 | 
					                lock_path: path.as_ref().to_path_buf(),
 | 
				
			||||||
 | 
					                ext: DistExtension::from_path(&path).map_err(|err| {
 | 
				
			||||||
 | 
					                    ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err)
 | 
				
			||||||
 | 
					                })?,
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        Ok(Self {
 | 
					        Ok(Self {
 | 
				
			||||||
| 
						 | 
					@ -181,15 +191,23 @@ pub struct ParsedPathUrl {
 | 
				
			||||||
    /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
					    /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
				
			||||||
    /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
					    /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
				
			||||||
    pub lock_path: PathBuf,
 | 
					    pub lock_path: PathBuf,
 | 
				
			||||||
 | 
					    /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
 | 
					    pub ext: DistExtension,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ParsedPathUrl {
 | 
					impl ParsedPathUrl {
 | 
				
			||||||
    /// Construct a [`ParsedPathUrl`] from a path requirement source.
 | 
					    /// Construct a [`ParsedPathUrl`] from a path requirement source.
 | 
				
			||||||
    pub fn from_source(install_path: PathBuf, lock_path: PathBuf, url: Url) -> Self {
 | 
					    pub fn from_source(
 | 
				
			||||||
 | 
					        install_path: PathBuf,
 | 
				
			||||||
 | 
					        lock_path: PathBuf,
 | 
				
			||||||
 | 
					        ext: DistExtension,
 | 
				
			||||||
 | 
					        url: Url,
 | 
				
			||||||
 | 
					    ) -> Self {
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            url,
 | 
					            url,
 | 
				
			||||||
            install_path,
 | 
					            install_path,
 | 
				
			||||||
            lock_path,
 | 
					            lock_path,
 | 
				
			||||||
 | 
					            ext,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -258,7 +276,7 @@ impl ParsedGitUrl {
 | 
				
			||||||
impl TryFrom<Url> for ParsedGitUrl {
 | 
					impl TryFrom<Url> for ParsedGitUrl {
 | 
				
			||||||
    type Error = ParsedUrlError;
 | 
					    type Error = ParsedUrlError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Supports URLS with and without the `git+` prefix.
 | 
					    /// Supports URLs with and without the `git+` prefix.
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
    /// When the URL includes a prefix, it's presumed to come from a PEP 508 requirement; when it's
 | 
					    /// When the URL includes a prefix, it's presumed to come from a PEP 508 requirement; when it's
 | 
				
			||||||
    /// excluded, it's presumed to come from `tool.uv.sources`.
 | 
					    /// excluded, it's presumed to come from `tool.uv.sources`.
 | 
				
			||||||
| 
						 | 
					@ -271,7 +289,7 @@ impl TryFrom<Url> for ParsedGitUrl {
 | 
				
			||||||
            .unwrap_or(url_in.as_str());
 | 
					            .unwrap_or(url_in.as_str());
 | 
				
			||||||
        let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?;
 | 
					        let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?;
 | 
				
			||||||
        let url = GitUrl::try_from(url)
 | 
					        let url = GitUrl::try_from(url)
 | 
				
			||||||
            .map_err(|err| ParsedUrlError::GitShaParse(url_in.clone(), err))?;
 | 
					            .map_err(|err| ParsedUrlError::GitShaParse(url_in.to_string(), err))?;
 | 
				
			||||||
        Ok(Self { url, subdirectory })
 | 
					        Ok(Self { url, subdirectory })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -286,22 +304,32 @@ impl TryFrom<Url> for ParsedGitUrl {
 | 
				
			||||||
pub struct ParsedArchiveUrl {
 | 
					pub struct ParsedArchiveUrl {
 | 
				
			||||||
    pub url: Url,
 | 
					    pub url: Url,
 | 
				
			||||||
    pub subdirectory: Option<PathBuf>,
 | 
					    pub subdirectory: Option<PathBuf>,
 | 
				
			||||||
 | 
					    pub ext: DistExtension,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ParsedArchiveUrl {
 | 
					impl ParsedArchiveUrl {
 | 
				
			||||||
    /// Construct a [`ParsedArchiveUrl`] from a URL requirement source.
 | 
					    /// Construct a [`ParsedArchiveUrl`] from a URL requirement source.
 | 
				
			||||||
    pub fn from_source(location: Url, subdirectory: Option<PathBuf>) -> Self {
 | 
					    pub fn from_source(location: Url, subdirectory: Option<PathBuf>, ext: DistExtension) -> Self {
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            url: location,
 | 
					            url: location,
 | 
				
			||||||
            subdirectory,
 | 
					            subdirectory,
 | 
				
			||||||
 | 
					            ext,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<Url> for ParsedArchiveUrl {
 | 
					impl TryFrom<Url> for ParsedArchiveUrl {
 | 
				
			||||||
    fn from(url: Url) -> Self {
 | 
					    type Error = ParsedUrlError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from(url: Url) -> Result<Self, Self::Error> {
 | 
				
			||||||
        let subdirectory = get_subdirectory(&url);
 | 
					        let subdirectory = get_subdirectory(&url);
 | 
				
			||||||
        Self { url, subdirectory }
 | 
					        let ext = DistExtension::from_path(url.path())
 | 
				
			||||||
 | 
					            .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?;
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            url,
 | 
				
			||||||
 | 
					            subdirectory,
 | 
				
			||||||
 | 
					            ext,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -328,22 +356,22 @@ impl TryFrom<Url> for ParsedUrl {
 | 
				
			||||||
                "git" => Ok(Self::Git(ParsedGitUrl::try_from(url)?)),
 | 
					                "git" => Ok(Self::Git(ParsedGitUrl::try_from(url)?)),
 | 
				
			||||||
                "bzr" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
					                "bzr" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
				
			||||||
                    prefix: prefix.to_string(),
 | 
					                    prefix: prefix.to_string(),
 | 
				
			||||||
                    url: url.clone(),
 | 
					                    url: url.to_string(),
 | 
				
			||||||
                    message: "Bazaar is not supported",
 | 
					                    message: "Bazaar is not supported",
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                "hg" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
					                "hg" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
				
			||||||
                    prefix: prefix.to_string(),
 | 
					                    prefix: prefix.to_string(),
 | 
				
			||||||
                    url: url.clone(),
 | 
					                    url: url.to_string(),
 | 
				
			||||||
                    message: "Mercurial is not supported",
 | 
					                    message: "Mercurial is not supported",
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                "svn" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
					                "svn" => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
				
			||||||
                    prefix: prefix.to_string(),
 | 
					                    prefix: prefix.to_string(),
 | 
				
			||||||
                    url: url.clone(),
 | 
					                    url: url.to_string(),
 | 
				
			||||||
                    message: "Subversion is not supported",
 | 
					                    message: "Subversion is not supported",
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                _ => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
					                _ => Err(ParsedUrlError::UnsupportedUrlPrefix {
 | 
				
			||||||
                    prefix: prefix.to_string(),
 | 
					                    prefix: prefix.to_string(),
 | 
				
			||||||
                    url: url.clone(),
 | 
					                    url: url.to_string(),
 | 
				
			||||||
                    message: "Unknown scheme",
 | 
					                    message: "Unknown scheme",
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -355,7 +383,7 @@ impl TryFrom<Url> for ParsedUrl {
 | 
				
			||||||
        } else if url.scheme().eq_ignore_ascii_case("file") {
 | 
					        } else if url.scheme().eq_ignore_ascii_case("file") {
 | 
				
			||||||
            let path = url
 | 
					            let path = url
 | 
				
			||||||
                .to_file_path()
 | 
					                .to_file_path()
 | 
				
			||||||
                .map_err(|()| ParsedUrlError::InvalidFileUrl(url.clone()))?;
 | 
					                .map_err(|()| ParsedUrlError::InvalidFileUrl(url.to_string()))?;
 | 
				
			||||||
            let is_dir = if let Ok(metadata) = path.metadata() {
 | 
					            let is_dir = if let Ok(metadata) = path.metadata() {
 | 
				
			||||||
                metadata.is_dir()
 | 
					                metadata.is_dir()
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
| 
						 | 
					@ -371,12 +399,14 @@ impl TryFrom<Url> for ParsedUrl {
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                Ok(Self::Path(ParsedPathUrl {
 | 
					                Ok(Self::Path(ParsedPathUrl {
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
 | 
					                    ext: DistExtension::from_path(&path)
 | 
				
			||||||
 | 
					                        .map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
 | 
				
			||||||
                    install_path: path.clone(),
 | 
					                    install_path: path.clone(),
 | 
				
			||||||
                    lock_path: path,
 | 
					                    lock_path: path,
 | 
				
			||||||
                }))
 | 
					                }))
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            Ok(Self::Archive(ParsedArchiveUrl::from(url)))
 | 
					            Ok(Self::Archive(ParsedArchiveUrl::try_from(url)?))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,17 +2,18 @@ use std::fmt::{Display, Formatter};
 | 
				
			||||||
use std::path::{Path, PathBuf};
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use thiserror::Error;
 | 
					use distribution_filename::DistExtension;
 | 
				
			||||||
use url::Url;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use pep440_rs::VersionSpecifiers;
 | 
					use pep440_rs::VersionSpecifiers;
 | 
				
			||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
 | 
					use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl};
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
use uv_fs::PortablePathBuf;
 | 
					use uv_fs::PortablePathBuf;
 | 
				
			||||||
use uv_git::{GitReference, GitSha, GitUrl};
 | 
					use uv_git::{GitReference, GitSha, GitUrl};
 | 
				
			||||||
use uv_normalize::{ExtraName, PackageName};
 | 
					use uv_normalize::{ExtraName, PackageName};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl,
 | 
					    ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, ParsedUrlError,
 | 
				
			||||||
 | 
					    VerbatimParsedUrl,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Error)]
 | 
					#[derive(Debug, Error)]
 | 
				
			||||||
| 
						 | 
					@ -20,6 +21,8 @@ pub enum RequirementError {
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
    VerbatimUrlError(#[from] pep508_rs::VerbatimUrlError),
 | 
					    VerbatimUrlError(#[from] pep508_rs::VerbatimUrlError),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    ParsedUrlError(#[from] ParsedUrlError),
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
    UrlParseError(#[from] url::ParseError),
 | 
					    UrlParseError(#[from] url::ParseError),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
    OidParseError(#[from] uv_git::OidParseError),
 | 
					    OidParseError(#[from] uv_git::OidParseError),
 | 
				
			||||||
| 
						 | 
					@ -95,13 +98,15 @@ impl From<Requirement> for pep508_rs::Requirement<VerbatimParsedUrl> {
 | 
				
			||||||
                    Some(VersionOrUrl::VersionSpecifier(specifier))
 | 
					                    Some(VersionOrUrl::VersionSpecifier(specifier))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                RequirementSource::Url {
 | 
					                RequirementSource::Url {
 | 
				
			||||||
                    subdirectory,
 | 
					 | 
				
			||||||
                    location,
 | 
					                    location,
 | 
				
			||||||
 | 
					                    subdirectory,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
 | 
					                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
 | 
				
			||||||
                    parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
 | 
					                    parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
 | 
				
			||||||
                        url: location,
 | 
					                        url: location,
 | 
				
			||||||
                        subdirectory,
 | 
					                        subdirectory,
 | 
				
			||||||
 | 
					                        ext,
 | 
				
			||||||
                    }),
 | 
					                    }),
 | 
				
			||||||
                    verbatim: url,
 | 
					                    verbatim: url,
 | 
				
			||||||
                })),
 | 
					                })),
 | 
				
			||||||
| 
						 | 
					@ -128,12 +133,14 @@ impl From<Requirement> for pep508_rs::Requirement<VerbatimParsedUrl> {
 | 
				
			||||||
                RequirementSource::Path {
 | 
					                RequirementSource::Path {
 | 
				
			||||||
                    install_path,
 | 
					                    install_path,
 | 
				
			||||||
                    lock_path,
 | 
					                    lock_path,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
 | 
					                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
 | 
				
			||||||
                    parsed_url: ParsedUrl::Path(ParsedPathUrl {
 | 
					                    parsed_url: ParsedUrl::Path(ParsedPathUrl {
 | 
				
			||||||
                        url: url.to_url(),
 | 
					                        url: url.to_url(),
 | 
				
			||||||
                        install_path,
 | 
					                        install_path,
 | 
				
			||||||
                        lock_path,
 | 
					                        lock_path,
 | 
				
			||||||
 | 
					                        ext,
 | 
				
			||||||
                    }),
 | 
					                    }),
 | 
				
			||||||
                    verbatim: url,
 | 
					                    verbatim: url,
 | 
				
			||||||
                })),
 | 
					                })),
 | 
				
			||||||
| 
						 | 
					@ -259,11 +266,13 @@ pub enum RequirementSource {
 | 
				
			||||||
    /// e.g. `foo @ https://example.org/foo-1.0-py3-none-any.whl`, or a source distribution,
 | 
					    /// e.g. `foo @ https://example.org/foo-1.0-py3-none-any.whl`, or a source distribution,
 | 
				
			||||||
    /// e.g.`foo @ https://example.org/foo-1.0.zip`.
 | 
					    /// e.g.`foo @ https://example.org/foo-1.0.zip`.
 | 
				
			||||||
    Url {
 | 
					    Url {
 | 
				
			||||||
 | 
					        /// The remote location of the archive file, without subdirectory fragment.
 | 
				
			||||||
 | 
					        location: Url,
 | 
				
			||||||
        /// For source distributions, the path to the distribution if it is not in the archive
 | 
					        /// For source distributions, the path to the distribution if it is not in the archive
 | 
				
			||||||
        /// root.
 | 
					        /// root.
 | 
				
			||||||
        subdirectory: Option<PathBuf>,
 | 
					        subdirectory: Option<PathBuf>,
 | 
				
			||||||
        /// The remote location of the archive file, without subdirectory fragment.
 | 
					        /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
        location: Url,
 | 
					        ext: DistExtension,
 | 
				
			||||||
        /// The PEP 508 style URL in the format
 | 
					        /// The PEP 508 style URL in the format
 | 
				
			||||||
        /// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`.
 | 
					        /// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`.
 | 
				
			||||||
        url: VerbatimUrl,
 | 
					        url: VerbatimUrl,
 | 
				
			||||||
| 
						 | 
					@ -292,6 +301,8 @@ pub enum RequirementSource {
 | 
				
			||||||
        /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
					        /// which we use for locking. Unlike `given` on the verbatim URL all environment variables
 | 
				
			||||||
        /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
					        /// are resolved, and unlike the install path, we did not yet join it on the base directory.
 | 
				
			||||||
        lock_path: PathBuf,
 | 
					        lock_path: PathBuf,
 | 
				
			||||||
 | 
					        /// The file extension, e.g. `tar.gz`, `zip`, etc.
 | 
				
			||||||
 | 
					        ext: DistExtension,
 | 
				
			||||||
        /// The PEP 508 style URL in the format
 | 
					        /// The PEP 508 style URL in the format
 | 
				
			||||||
        /// `file:///<path>#subdirectory=<subdirectory>`.
 | 
					        /// `file:///<path>#subdirectory=<subdirectory>`.
 | 
				
			||||||
        url: VerbatimUrl,
 | 
					        url: VerbatimUrl,
 | 
				
			||||||
| 
						 | 
					@ -321,6 +332,7 @@ impl RequirementSource {
 | 
				
			||||||
            ParsedUrl::Path(local_file) => RequirementSource::Path {
 | 
					            ParsedUrl::Path(local_file) => RequirementSource::Path {
 | 
				
			||||||
                install_path: local_file.install_path.clone(),
 | 
					                install_path: local_file.install_path.clone(),
 | 
				
			||||||
                lock_path: local_file.lock_path.clone(),
 | 
					                lock_path: local_file.lock_path.clone(),
 | 
				
			||||||
 | 
					                ext: local_file.ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            ParsedUrl::Directory(directory) => RequirementSource::Directory {
 | 
					            ParsedUrl::Directory(directory) => RequirementSource::Directory {
 | 
				
			||||||
| 
						 | 
					@ -340,6 +352,7 @@ impl RequirementSource {
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
                location: archive.url,
 | 
					                location: archive.url,
 | 
				
			||||||
                subdirectory: archive.subdirectory,
 | 
					                subdirectory: archive.subdirectory,
 | 
				
			||||||
 | 
					                ext: archive.ext,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -347,7 +360,7 @@ impl RequirementSource {
 | 
				
			||||||
    /// Construct a [`RequirementSource`] for a URL source, given a URL parsed into components.
 | 
					    /// Construct a [`RequirementSource`] for a URL source, given a URL parsed into components.
 | 
				
			||||||
    pub fn from_verbatim_parsed_url(parsed_url: ParsedUrl) -> Self {
 | 
					    pub fn from_verbatim_parsed_url(parsed_url: ParsedUrl) -> Self {
 | 
				
			||||||
        let verbatim_url = VerbatimUrl::from_url(Url::from(parsed_url.clone()));
 | 
					        let verbatim_url = VerbatimUrl::from_url(Url::from(parsed_url.clone()));
 | 
				
			||||||
        RequirementSource::from_parsed_url(parsed_url, verbatim_url)
 | 
					        Self::from_parsed_url(parsed_url, verbatim_url)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Convert the source to a [`VerbatimParsedUrl`], if it's a URL source.
 | 
					    /// Convert the source to a [`VerbatimParsedUrl`], if it's a URL source.
 | 
				
			||||||
| 
						 | 
					@ -355,24 +368,28 @@ impl RequirementSource {
 | 
				
			||||||
        match &self {
 | 
					        match &self {
 | 
				
			||||||
            Self::Registry { .. } => None,
 | 
					            Self::Registry { .. } => None,
 | 
				
			||||||
            Self::Url {
 | 
					            Self::Url {
 | 
				
			||||||
                subdirectory,
 | 
					 | 
				
			||||||
                location,
 | 
					                location,
 | 
				
			||||||
 | 
					                subdirectory,
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
            } => Some(VerbatimParsedUrl {
 | 
					            } => Some(VerbatimParsedUrl {
 | 
				
			||||||
                parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
 | 
					                parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
 | 
				
			||||||
                    location.clone(),
 | 
					                    location.clone(),
 | 
				
			||||||
                    subdirectory.clone(),
 | 
					                    subdirectory.clone(),
 | 
				
			||||||
 | 
					                    *ext,
 | 
				
			||||||
                )),
 | 
					                )),
 | 
				
			||||||
                verbatim: url.clone(),
 | 
					                verbatim: url.clone(),
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            Self::Path {
 | 
					            Self::Path {
 | 
				
			||||||
                install_path,
 | 
					                install_path,
 | 
				
			||||||
                lock_path,
 | 
					                lock_path,
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
            } => Some(VerbatimParsedUrl {
 | 
					            } => Some(VerbatimParsedUrl {
 | 
				
			||||||
                parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
 | 
					                parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
 | 
				
			||||||
                    install_path.clone(),
 | 
					                    install_path.clone(),
 | 
				
			||||||
                    lock_path.clone(),
 | 
					                    lock_path.clone(),
 | 
				
			||||||
 | 
					                    *ext,
 | 
				
			||||||
                    url.to_url(),
 | 
					                    url.to_url(),
 | 
				
			||||||
                )),
 | 
					                )),
 | 
				
			||||||
                verbatim: url.clone(),
 | 
					                verbatim: url.clone(),
 | 
				
			||||||
| 
						 | 
					@ -504,6 +521,7 @@ impl From<RequirementSource> for RequirementSourceWire {
 | 
				
			||||||
            RequirementSource::Url {
 | 
					            RequirementSource::Url {
 | 
				
			||||||
                subdirectory,
 | 
					                subdirectory,
 | 
				
			||||||
                location,
 | 
					                location,
 | 
				
			||||||
 | 
					                ext: _,
 | 
				
			||||||
                url: _,
 | 
					                url: _,
 | 
				
			||||||
            } => Self::Direct {
 | 
					            } => Self::Direct {
 | 
				
			||||||
                url: location,
 | 
					                url: location,
 | 
				
			||||||
| 
						 | 
					@ -564,6 +582,7 @@ impl From<RequirementSource> for RequirementSourceWire {
 | 
				
			||||||
            RequirementSource::Path {
 | 
					            RequirementSource::Path {
 | 
				
			||||||
                install_path,
 | 
					                install_path,
 | 
				
			||||||
                lock_path: _,
 | 
					                lock_path: _,
 | 
				
			||||||
 | 
					                ext: _,
 | 
				
			||||||
                url: _,
 | 
					                url: _,
 | 
				
			||||||
            } => Self::Path {
 | 
					            } => Self::Path {
 | 
				
			||||||
                path: PortablePathBuf::from(install_path),
 | 
					                path: PortablePathBuf::from(install_path),
 | 
				
			||||||
| 
						 | 
					@ -626,13 +645,17 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            RequirementSourceWire::Direct { url, subdirectory } => Ok(Self::Url {
 | 
					            RequirementSourceWire::Direct { url, subdirectory } => Ok(Self::Url {
 | 
				
			||||||
                url: VerbatimUrl::from_url(url.clone()),
 | 
					                url: VerbatimUrl::from_url(url.clone()),
 | 
				
			||||||
                subdirectory: subdirectory.map(PathBuf::from),
 | 
					 | 
				
			||||||
                location: url.clone(),
 | 
					                location: url.clone(),
 | 
				
			||||||
 | 
					                subdirectory: subdirectory.map(PathBuf::from),
 | 
				
			||||||
 | 
					                ext: DistExtension::from_path(url.path())
 | 
				
			||||||
 | 
					                    .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
            RequirementSourceWire::Path { path } => {
 | 
					            RequirementSourceWire::Path { path } => {
 | 
				
			||||||
                let path = PathBuf::from(path);
 | 
					                let path = PathBuf::from(path);
 | 
				
			||||||
                Ok(Self::Path {
 | 
					                Ok(Self::Path {
 | 
				
			||||||
                    url: VerbatimUrl::from_path(path.as_path())?,
 | 
					                    url: VerbatimUrl::from_path(path.as_path())?,
 | 
				
			||||||
 | 
					                    ext: DistExtension::from_path(path.as_path())
 | 
				
			||||||
 | 
					                        .map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
 | 
				
			||||||
                    install_path: path.clone(),
 | 
					                    install_path: path.clone(),
 | 
				
			||||||
                    lock_path: path,
 | 
					                    lock_path: path,
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1459,7 +1459,40 @@ mod test {
 | 
				
			||||||
        let temp_dir = assert_fs::TempDir::new()?;
 | 
					        let temp_dir = assert_fs::TempDir::new()?;
 | 
				
			||||||
        let requirements_txt = temp_dir.child("requirements.txt");
 | 
					        let requirements_txt = temp_dir.child("requirements.txt");
 | 
				
			||||||
        requirements_txt.write_str(indoc! {"
 | 
					        requirements_txt.write_str(indoc! {"
 | 
				
			||||||
            -e http://localhost:8080/
 | 
					            -e https://localhost:8080/
 | 
				
			||||||
 | 
					        "})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let error = RequirementsTxt::parse(
 | 
				
			||||||
 | 
					            requirements_txt.path(),
 | 
				
			||||||
 | 
					            temp_dir.path(),
 | 
				
			||||||
 | 
					            &BaseClientBuilder::new(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap_err();
 | 
				
			||||||
 | 
					        let errors = anyhow::Error::new(error).chain().join("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let requirement_txt = regex::escape(&requirements_txt.path().user_display().to_string());
 | 
				
			||||||
 | 
					        let filters = vec![(requirement_txt.as_str(), "<REQUIREMENTS_TXT>")];
 | 
				
			||||||
 | 
					        insta::with_settings!({
 | 
				
			||||||
 | 
					            filters => filters
 | 
				
			||||||
 | 
					        }, {
 | 
				
			||||||
 | 
					            insta::assert_snapshot!(errors, @r###"
 | 
				
			||||||
 | 
					            Couldn't parse requirement in `<REQUIREMENTS_TXT>` at position 3
 | 
				
			||||||
 | 
					            Expected direct URL (`https://localhost:8080/`) to end in a supported file extension: `.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`
 | 
				
			||||||
 | 
					            https://localhost:8080/
 | 
				
			||||||
 | 
					            ^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					            "###);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn unsupported_editable_extension() -> Result<()> {
 | 
				
			||||||
 | 
					        let temp_dir = assert_fs::TempDir::new()?;
 | 
				
			||||||
 | 
					        let requirements_txt = temp_dir.child("requirements.txt");
 | 
				
			||||||
 | 
					        requirements_txt.write_str(indoc! {"
 | 
				
			||||||
 | 
					            -e https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.gz
 | 
				
			||||||
        "})?;
 | 
					        "})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let error = RequirementsTxt::parse(
 | 
					        let error = RequirementsTxt::parse(
 | 
				
			||||||
| 
						 | 
					@ -1478,7 +1511,7 @@ mod test {
 | 
				
			||||||
        }, {
 | 
					        }, {
 | 
				
			||||||
            insta::assert_snapshot!(errors, @r###"
 | 
					            insta::assert_snapshot!(errors, @r###"
 | 
				
			||||||
            Unsupported editable requirement in `<REQUIREMENTS_TXT>`
 | 
					            Unsupported editable requirement in `<REQUIREMENTS_TXT>`
 | 
				
			||||||
            Editable must refer to a local directory, not an HTTPS URL: `http://localhost:8080/`
 | 
					            Editable must refer to a local directory, not an HTTPS URL: `https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.gz`
 | 
				
			||||||
            "###);
 | 
					            "###);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,9 +36,10 @@ RequirementsTxt {
 | 
				
			||||||
                    version_or_url: Some(
 | 
					                    version_or_url: Some(
 | 
				
			||||||
                        Url(
 | 
					                        Url(
 | 
				
			||||||
                            VerbatimParsedUrl {
 | 
					                            VerbatimParsedUrl {
 | 
				
			||||||
                                parsed_url: Archive(
 | 
					                                parsed_url: Git(
 | 
				
			||||||
                                    ParsedArchiveUrl {
 | 
					                                    ParsedGitUrl {
 | 
				
			||||||
                                        url: Url {
 | 
					                                        url: GitUrl {
 | 
				
			||||||
 | 
					                                            repository: Url {
 | 
				
			||||||
                                                scheme: "https",
 | 
					                                                scheme: "https",
 | 
				
			||||||
                                                cannot_be_a_base: false,
 | 
					                                                cannot_be_a_base: false,
 | 
				
			||||||
                                                username: "",
 | 
					                                                username: "",
 | 
				
			||||||
| 
						 | 
					@ -49,16 +50,19 @@ RequirementsTxt {
 | 
				
			||||||
                                                    ),
 | 
					                                                    ),
 | 
				
			||||||
                                                ),
 | 
					                                                ),
 | 
				
			||||||
                                                port: None,
 | 
					                                                port: None,
 | 
				
			||||||
                                            path: "/pandas-dev/pandas",
 | 
					                                                path: "/pandas-dev/pandas.git",
 | 
				
			||||||
                                                query: None,
 | 
					                                                query: None,
 | 
				
			||||||
                                                fragment: None,
 | 
					                                                fragment: None,
 | 
				
			||||||
                                            },
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            reference: DefaultBranch,
 | 
				
			||||||
 | 
					                                            precise: None,
 | 
				
			||||||
 | 
					                                        },
 | 
				
			||||||
                                        subdirectory: None,
 | 
					                                        subdirectory: None,
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                                verbatim: VerbatimUrl {
 | 
					                                verbatim: VerbatimUrl {
 | 
				
			||||||
                                    url: Url {
 | 
					                                    url: Url {
 | 
				
			||||||
                                        scheme: "https",
 | 
					                                        scheme: "git+https",
 | 
				
			||||||
                                        cannot_be_a_base: false,
 | 
					                                        cannot_be_a_base: false,
 | 
				
			||||||
                                        username: "",
 | 
					                                        username: "",
 | 
				
			||||||
                                        password: None,
 | 
					                                        password: None,
 | 
				
			||||||
| 
						 | 
					@ -68,12 +72,12 @@ RequirementsTxt {
 | 
				
			||||||
                                            ),
 | 
					                                            ),
 | 
				
			||||||
                                        ),
 | 
					                                        ),
 | 
				
			||||||
                                        port: None,
 | 
					                                        port: None,
 | 
				
			||||||
                                        path: "/pandas-dev/pandas",
 | 
					                                        path: "/pandas-dev/pandas.git",
 | 
				
			||||||
                                        query: None,
 | 
					                                        query: None,
 | 
				
			||||||
                                        fragment: None,
 | 
					                                        fragment: None,
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                    given: Some(
 | 
					                                    given: Some(
 | 
				
			||||||
                                        "https://github.com/pandas-dev/pandas",
 | 
					                                        "git+https://github.com/pandas-dev/pandas.git",
 | 
				
			||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,9 +36,10 @@ RequirementsTxt {
 | 
				
			||||||
                    version_or_url: Some(
 | 
					                    version_or_url: Some(
 | 
				
			||||||
                        Url(
 | 
					                        Url(
 | 
				
			||||||
                            VerbatimParsedUrl {
 | 
					                            VerbatimParsedUrl {
 | 
				
			||||||
                                parsed_url: Archive(
 | 
					                                parsed_url: Git(
 | 
				
			||||||
                                    ParsedArchiveUrl {
 | 
					                                    ParsedGitUrl {
 | 
				
			||||||
                                        url: Url {
 | 
					                                        url: GitUrl {
 | 
				
			||||||
 | 
					                                            repository: Url {
 | 
				
			||||||
                                                scheme: "https",
 | 
					                                                scheme: "https",
 | 
				
			||||||
                                                cannot_be_a_base: false,
 | 
					                                                cannot_be_a_base: false,
 | 
				
			||||||
                                                username: "",
 | 
					                                                username: "",
 | 
				
			||||||
| 
						 | 
					@ -49,16 +50,19 @@ RequirementsTxt {
 | 
				
			||||||
                                                    ),
 | 
					                                                    ),
 | 
				
			||||||
                                                ),
 | 
					                                                ),
 | 
				
			||||||
                                                port: None,
 | 
					                                                port: None,
 | 
				
			||||||
                                            path: "/pandas-dev/pandas",
 | 
					                                                path: "/pandas-dev/pandas.git",
 | 
				
			||||||
                                                query: None,
 | 
					                                                query: None,
 | 
				
			||||||
                                                fragment: None,
 | 
					                                                fragment: None,
 | 
				
			||||||
                                            },
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            reference: DefaultBranch,
 | 
				
			||||||
 | 
					                                            precise: None,
 | 
				
			||||||
 | 
					                                        },
 | 
				
			||||||
                                        subdirectory: None,
 | 
					                                        subdirectory: None,
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                                verbatim: VerbatimUrl {
 | 
					                                verbatim: VerbatimUrl {
 | 
				
			||||||
                                    url: Url {
 | 
					                                    url: Url {
 | 
				
			||||||
                                        scheme: "https",
 | 
					                                        scheme: "git+https",
 | 
				
			||||||
                                        cannot_be_a_base: false,
 | 
					                                        cannot_be_a_base: false,
 | 
				
			||||||
                                        username: "",
 | 
					                                        username: "",
 | 
				
			||||||
                                        password: None,
 | 
					                                        password: None,
 | 
				
			||||||
| 
						 | 
					@ -68,12 +72,12 @@ RequirementsTxt {
 | 
				
			||||||
                                            ),
 | 
					                                            ),
 | 
				
			||||||
                                        ),
 | 
					                                        ),
 | 
				
			||||||
                                        port: None,
 | 
					                                        port: None,
 | 
				
			||||||
                                        path: "/pandas-dev/pandas",
 | 
					                                        path: "/pandas-dev/pandas.git",
 | 
				
			||||||
                                        query: None,
 | 
					                                        query: None,
 | 
				
			||||||
                                        fragment: None,
 | 
					                                        fragment: None,
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                    given: Some(
 | 
					                                    given: Some(
 | 
				
			||||||
                                        "https://github.com/pandas-dev/pandas",
 | 
					                                        "git+https://github.com/pandas-dev/pandas.git",
 | 
				
			||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\
 | 
					\
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pandas  [tabulate]  @  https://github.com/pandas-dev/pandas \
 | 
					pandas  [tabulate]  @  git+https://github.com/pandas-dev/pandas.git \
 | 
				
			||||||
 # üh
 | 
					 # üh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -691,7 +691,7 @@ impl CacheBucket {
 | 
				
			||||||
            Self::Interpreter => "interpreter-v2",
 | 
					            Self::Interpreter => "interpreter-v2",
 | 
				
			||||||
            // Note that when bumping this, you'll also need to bump it
 | 
					            // Note that when bumping this, you'll also need to bump it
 | 
				
			||||||
            // in crates/uv/tests/cache_clean.rs.
 | 
					            // in crates/uv/tests/cache_clean.rs.
 | 
				
			||||||
            Self::Simple => "simple-v11",
 | 
					            Self::Simple => "simple-v12",
 | 
				
			||||||
            Self::Wheels => "wheels-v1",
 | 
					            Self::Wheels => "wheels-v1",
 | 
				
			||||||
            Self::Archive => "archive-v0",
 | 
					            Self::Archive => "archive-v0",
 | 
				
			||||||
            Self::Builds => "builds-v0",
 | 
					            Self::Builds => "builds-v0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -718,9 +718,12 @@ impl SimpleMetadata {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Group the distributions by version and kind
 | 
					        // Group the distributions by version and kind
 | 
				
			||||||
        for file in files {
 | 
					        for file in files {
 | 
				
			||||||
            if let Some(filename) =
 | 
					            let Some(filename) =
 | 
				
			||||||
                DistFilename::try_from_filename(file.filename.as_str(), package_name)
 | 
					                DistFilename::try_from_filename(file.filename.as_str(), package_name)
 | 
				
			||||||
            {
 | 
					            else {
 | 
				
			||||||
 | 
					                warn!("Skipping file for {package_name}: {}", file.filename);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
            let version = match filename {
 | 
					            let version = match filename {
 | 
				
			||||||
                DistFilename::SourceDistFilename(ref inner) => &inner.version,
 | 
					                DistFilename::SourceDistFilename(ref inner) => &inner.version,
 | 
				
			||||||
                DistFilename::WheelFilename(ref inner) => &inner.version,
 | 
					                DistFilename::WheelFilename(ref inner) => &inner.version,
 | 
				
			||||||
| 
						 | 
					@ -744,7 +747,6 @@ impl SimpleMetadata {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Self(
 | 
					        Self(
 | 
				
			||||||
            map.into_iter()
 | 
					            map.into_iter()
 | 
				
			||||||
                .map(|(version, files)| SimpleMetadatum { version, files })
 | 
					                .map(|(version, files)| SimpleMetadatum { version, files })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ use zip::result::ZipError;
 | 
				
			||||||
use crate::metadata::MetadataError;
 | 
					use crate::metadata::MetadataError;
 | 
				
			||||||
use distribution_filename::WheelFilenameError;
 | 
					use distribution_filename::WheelFilenameError;
 | 
				
			||||||
use pep440_rs::Version;
 | 
					use pep440_rs::Version;
 | 
				
			||||||
use pypi_types::HashDigest;
 | 
					use pypi_types::{HashDigest, ParsedUrlError};
 | 
				
			||||||
use uv_client::WrappedReqwestError;
 | 
					use uv_client::WrappedReqwestError;
 | 
				
			||||||
use uv_fs::Simplified;
 | 
					use uv_fs::Simplified;
 | 
				
			||||||
use uv_normalize::PackageName;
 | 
					use uv_normalize::PackageName;
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,8 @@ pub enum Error {
 | 
				
			||||||
    #[error("Expected an absolute path, but received: {}", _0.user_display())]
 | 
					    #[error("Expected an absolute path, but received: {}", _0.user_display())]
 | 
				
			||||||
    RelativePath(PathBuf),
 | 
					    RelativePath(PathBuf),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    ParsedUrl(#[from] ParsedUrlError),
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
    JoinRelativeUrl(#[from] pypi_types::JoinRelativeError),
 | 
					    JoinRelativeUrl(#[from] pypi_types::JoinRelativeError),
 | 
				
			||||||
    #[error("Expected a file URL, but received: {0}")]
 | 
					    #[error("Expected a file URL, but received: {0}")]
 | 
				
			||||||
    NonFileUrl(Url),
 | 
					    NonFileUrl(Url),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,13 @@ use std::collections::BTreeMap;
 | 
				
			||||||
use std::io;
 | 
					use std::io;
 | 
				
			||||||
use std::path::{Path, PathBuf};
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use distribution_filename::DistExtension;
 | 
				
			||||||
use path_absolutize::Absolutize;
 | 
					use path_absolutize::Absolutize;
 | 
				
			||||||
use thiserror::Error;
 | 
					 | 
				
			||||||
use url::Url;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use pep440_rs::VersionSpecifiers;
 | 
					use pep440_rs::VersionSpecifiers;
 | 
				
			||||||
use pep508_rs::{VerbatimUrl, VersionOrUrl};
 | 
					use pep508_rs::{VerbatimUrl, VersionOrUrl};
 | 
				
			||||||
use pypi_types::{Requirement, RequirementSource, VerbatimParsedUrl};
 | 
					use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl};
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					use url::Url;
 | 
				
			||||||
use uv_configuration::PreviewMode;
 | 
					use uv_configuration::PreviewMode;
 | 
				
			||||||
use uv_fs::{relative_to, Simplified};
 | 
					use uv_fs::{relative_to, Simplified};
 | 
				
			||||||
use uv_git::GitReference;
 | 
					use uv_git::GitReference;
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,8 @@ pub enum LoweringError {
 | 
				
			||||||
    WorkspaceFalse,
 | 
					    WorkspaceFalse,
 | 
				
			||||||
    #[error("Editable must refer to a local directory, not a file: `{0}`")]
 | 
					    #[error("Editable must refer to a local directory, not a file: `{0}`")]
 | 
				
			||||||
    EditableFile(String),
 | 
					    EditableFile(String),
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    ParsedUrl(#[from] ParsedUrlError),
 | 
				
			||||||
    #[error(transparent)] // Function attaches the context
 | 
					    #[error(transparent)] // Function attaches the context
 | 
				
			||||||
    RelativeTo(io::Error),
 | 
					    RelativeTo(io::Error),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -155,10 +157,14 @@ pub(crate) fn lower_requirement(
 | 
				
			||||||
                verbatim_url.set_fragment(Some(subdirectory));
 | 
					                verbatim_url.set_fragment(Some(subdirectory));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let ext = DistExtension::from_path(url.path())
 | 
				
			||||||
 | 
					                .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let verbatim_url = VerbatimUrl::from_url(verbatim_url);
 | 
					            let verbatim_url = VerbatimUrl::from_url(verbatim_url);
 | 
				
			||||||
            RequirementSource::Url {
 | 
					            RequirementSource::Url {
 | 
				
			||||||
                location: url,
 | 
					                location: url,
 | 
				
			||||||
                subdirectory: subdirectory.map(PathBuf::from),
 | 
					                subdirectory: subdirectory.map(PathBuf::from),
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url: verbatim_url,
 | 
					                url: verbatim_url,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -290,6 +296,8 @@ fn path_source(
 | 
				
			||||||
        Ok(RequirementSource::Path {
 | 
					        Ok(RequirementSource::Path {
 | 
				
			||||||
            install_path: absolute_path,
 | 
					            install_path: absolute_path,
 | 
				
			||||||
            lock_path: relative_to_workspace,
 | 
					            lock_path: relative_to_workspace,
 | 
				
			||||||
 | 
					            ext: DistExtension::from_path(path)
 | 
				
			||||||
 | 
					                .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
 | 
				
			||||||
            url,
 | 
					            url,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,14 +13,14 @@ use tracing::{debug, info_span, instrument, Instrument};
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
use zip::ZipArchive;
 | 
					use zip::ZipArchive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use distribution_filename::WheelFilename;
 | 
					use distribution_filename::{SourceDistExtension, WheelFilename};
 | 
				
			||||||
use distribution_types::{
 | 
					use distribution_types::{
 | 
				
			||||||
    BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed,
 | 
					    BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed,
 | 
				
			||||||
    PathSourceUrl, RemoteSource, SourceDist, SourceUrl,
 | 
					    PathSourceUrl, RemoteSource, SourceDist, SourceUrl,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use install_wheel_rs::metadata::read_archive_metadata;
 | 
					use install_wheel_rs::metadata::read_archive_metadata;
 | 
				
			||||||
use platform_tags::Tags;
 | 
					use platform_tags::Tags;
 | 
				
			||||||
use pypi_types::{HashDigest, Metadata23, ParsedArchiveUrl};
 | 
					use pypi_types::{HashDigest, Metadata23};
 | 
				
			||||||
use uv_cache::{
 | 
					use uv_cache::{
 | 
				
			||||||
    ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Timestamp, WheelCache,
 | 
					    ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Timestamp, WheelCache,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -111,6 +111,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                                &PathSourceUrl {
 | 
					                                &PathSourceUrl {
 | 
				
			||||||
                                    url: &url,
 | 
					                                    url: &url,
 | 
				
			||||||
                                    path: Cow::Borrowed(path),
 | 
					                                    path: Cow::Borrowed(path),
 | 
				
			||||||
 | 
					                                    ext: dist.ext,
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                                &cache_shard,
 | 
					                                &cache_shard,
 | 
				
			||||||
                                tags,
 | 
					                                tags,
 | 
				
			||||||
| 
						 | 
					@ -132,6 +133,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                            &PathSourceUrl {
 | 
					                            &PathSourceUrl {
 | 
				
			||||||
                                url: &url,
 | 
					                                url: &url,
 | 
				
			||||||
                                path: Cow::Owned(path),
 | 
					                                path: Cow::Owned(path),
 | 
				
			||||||
 | 
					                                ext: dist.ext,
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                            &cache_shard,
 | 
					                            &cache_shard,
 | 
				
			||||||
                            tags,
 | 
					                            tags,
 | 
				
			||||||
| 
						 | 
					@ -147,6 +149,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                    &url,
 | 
					                    &url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    None,
 | 
					                    None,
 | 
				
			||||||
 | 
					                    dist.ext,
 | 
				
			||||||
                    tags,
 | 
					                    tags,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
| 
						 | 
					@ -156,21 +159,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
 | 
					            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
 | 
				
			||||||
                let filename = dist.filename().expect("Distribution must have a filename");
 | 
					                let filename = dist.filename().expect("Distribution must have a filename");
 | 
				
			||||||
                let ParsedArchiveUrl { url, subdirectory } =
 | 
					 | 
				
			||||||
                    ParsedArchiveUrl::from(dist.url.to_url());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // For direct URLs, cache directly under the hash of the URL itself.
 | 
					                // For direct URLs, cache directly under the hash of the URL itself.
 | 
				
			||||||
                let cache_shard = self.build_context.cache().shard(
 | 
					                let cache_shard = self.build_context.cache().shard(
 | 
				
			||||||
                    CacheBucket::SourceDistributions,
 | 
					                    CacheBucket::SourceDistributions,
 | 
				
			||||||
                    WheelCache::Url(&url).root(),
 | 
					                    WheelCache::Url(&dist.url).root(),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.url(
 | 
					                self.url(
 | 
				
			||||||
                    source,
 | 
					                    source,
 | 
				
			||||||
                    &filename,
 | 
					                    &filename,
 | 
				
			||||||
                    &url,
 | 
					                    &dist.url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    subdirectory.as_deref(),
 | 
					                    dist.subdirectory.as_deref(),
 | 
				
			||||||
 | 
					                    dist.ext,
 | 
				
			||||||
                    tags,
 | 
					                    tags,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
| 
						 | 
					@ -208,21 +210,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                    .url
 | 
					                    .url
 | 
				
			||||||
                    .filename()
 | 
					                    .filename()
 | 
				
			||||||
                    .expect("Distribution must have a filename");
 | 
					                    .expect("Distribution must have a filename");
 | 
				
			||||||
                let ParsedArchiveUrl { url, subdirectory } =
 | 
					 | 
				
			||||||
                    ParsedArchiveUrl::from(resource.url.clone());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // For direct URLs, cache directly under the hash of the URL itself.
 | 
					                // For direct URLs, cache directly under the hash of the URL itself.
 | 
				
			||||||
                let cache_shard = self.build_context.cache().shard(
 | 
					                let cache_shard = self.build_context.cache().shard(
 | 
				
			||||||
                    CacheBucket::SourceDistributions,
 | 
					                    CacheBucket::SourceDistributions,
 | 
				
			||||||
                    WheelCache::Url(&url).root(),
 | 
					                    WheelCache::Url(resource.url).root(),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.url(
 | 
					                self.url(
 | 
				
			||||||
                    source,
 | 
					                    source,
 | 
				
			||||||
                    &filename,
 | 
					                    &filename,
 | 
				
			||||||
                    &url,
 | 
					                    resource.url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    subdirectory.as_deref(),
 | 
					                    resource.subdirectory,
 | 
				
			||||||
 | 
					                    resource.ext,
 | 
				
			||||||
                    tags,
 | 
					                    tags,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
| 
						 | 
					@ -287,6 +288,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                                &PathSourceUrl {
 | 
					                                &PathSourceUrl {
 | 
				
			||||||
                                    url: &url,
 | 
					                                    url: &url,
 | 
				
			||||||
                                    path: Cow::Borrowed(path),
 | 
					                                    path: Cow::Borrowed(path),
 | 
				
			||||||
 | 
					                                    ext: dist.ext,
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                                &cache_shard,
 | 
					                                &cache_shard,
 | 
				
			||||||
                                hashes,
 | 
					                                hashes,
 | 
				
			||||||
| 
						 | 
					@ -307,6 +309,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                            &PathSourceUrl {
 | 
					                            &PathSourceUrl {
 | 
				
			||||||
                                url: &url,
 | 
					                                url: &url,
 | 
				
			||||||
                                path: Cow::Owned(path),
 | 
					                                path: Cow::Owned(path),
 | 
				
			||||||
 | 
					                                ext: dist.ext,
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                            &cache_shard,
 | 
					                            &cache_shard,
 | 
				
			||||||
                            hashes,
 | 
					                            hashes,
 | 
				
			||||||
| 
						 | 
					@ -321,6 +324,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                    &url,
 | 
					                    &url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    None,
 | 
					                    None,
 | 
				
			||||||
 | 
					                    dist.ext,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
| 
						 | 
					@ -329,21 +333,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
 | 
					            BuildableSource::Dist(SourceDist::DirectUrl(dist)) => {
 | 
				
			||||||
                let filename = dist.filename().expect("Distribution must have a filename");
 | 
					                let filename = dist.filename().expect("Distribution must have a filename");
 | 
				
			||||||
                let ParsedArchiveUrl { url, subdirectory } =
 | 
					 | 
				
			||||||
                    ParsedArchiveUrl::from(dist.url.to_url());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // For direct URLs, cache directly under the hash of the URL itself.
 | 
					                // For direct URLs, cache directly under the hash of the URL itself.
 | 
				
			||||||
                let cache_shard = self.build_context.cache().shard(
 | 
					                let cache_shard = self.build_context.cache().shard(
 | 
				
			||||||
                    CacheBucket::SourceDistributions,
 | 
					                    CacheBucket::SourceDistributions,
 | 
				
			||||||
                    WheelCache::Url(&url).root(),
 | 
					                    WheelCache::Url(&dist.url).root(),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.url_metadata(
 | 
					                self.url_metadata(
 | 
				
			||||||
                    source,
 | 
					                    source,
 | 
				
			||||||
                    &filename,
 | 
					                    &filename,
 | 
				
			||||||
                    &url,
 | 
					                    &dist.url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    subdirectory.as_deref(),
 | 
					                    dist.subdirectory.as_deref(),
 | 
				
			||||||
 | 
					                    dist.ext,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
| 
						 | 
					@ -374,21 +377,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                    .url
 | 
					                    .url
 | 
				
			||||||
                    .filename()
 | 
					                    .filename()
 | 
				
			||||||
                    .expect("Distribution must have a filename");
 | 
					                    .expect("Distribution must have a filename");
 | 
				
			||||||
                let ParsedArchiveUrl { url, subdirectory } =
 | 
					 | 
				
			||||||
                    ParsedArchiveUrl::from(resource.url.clone());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // For direct URLs, cache directly under the hash of the URL itself.
 | 
					                // For direct URLs, cache directly under the hash of the URL itself.
 | 
				
			||||||
                let cache_shard = self.build_context.cache().shard(
 | 
					                let cache_shard = self.build_context.cache().shard(
 | 
				
			||||||
                    CacheBucket::SourceDistributions,
 | 
					                    CacheBucket::SourceDistributions,
 | 
				
			||||||
                    WheelCache::Url(&url).root(),
 | 
					                    WheelCache::Url(resource.url).root(),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.url_metadata(
 | 
					                self.url_metadata(
 | 
				
			||||||
                    source,
 | 
					                    source,
 | 
				
			||||||
                    &filename,
 | 
					                    &filename,
 | 
				
			||||||
                    &url,
 | 
					                    resource.url,
 | 
				
			||||||
                    &cache_shard,
 | 
					                    &cache_shard,
 | 
				
			||||||
                    subdirectory.as_deref(),
 | 
					                    resource.subdirectory,
 | 
				
			||||||
 | 
					                    resource.ext,
 | 
				
			||||||
                    hashes,
 | 
					                    hashes,
 | 
				
			||||||
                    client,
 | 
					                    client,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
| 
						 | 
					@ -441,6 +443,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        url: &'data Url,
 | 
					        url: &'data Url,
 | 
				
			||||||
        cache_shard: &CacheShard,
 | 
					        cache_shard: &CacheShard,
 | 
				
			||||||
        subdirectory: Option<&'data Path>,
 | 
					        subdirectory: Option<&'data Path>,
 | 
				
			||||||
 | 
					        ext: SourceDistExtension,
 | 
				
			||||||
        tags: &Tags,
 | 
					        tags: &Tags,
 | 
				
			||||||
        hashes: HashPolicy<'_>,
 | 
					        hashes: HashPolicy<'_>,
 | 
				
			||||||
        client: &ManagedClient<'_>,
 | 
					        client: &ManagedClient<'_>,
 | 
				
			||||||
| 
						 | 
					@ -449,7 +452,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Fetch the revision for the source distribution.
 | 
					        // Fetch the revision for the source distribution.
 | 
				
			||||||
        let revision = self
 | 
					        let revision = self
 | 
				
			||||||
            .url_revision(source, filename, url, cache_shard, hashes, client)
 | 
					            .url_revision(source, filename, ext, url, cache_shard, hashes, client)
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Before running the build, check that the hashes match.
 | 
					        // Before running the build, check that the hashes match.
 | 
				
			||||||
| 
						 | 
					@ -512,6 +515,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        url: &'data Url,
 | 
					        url: &'data Url,
 | 
				
			||||||
        cache_shard: &CacheShard,
 | 
					        cache_shard: &CacheShard,
 | 
				
			||||||
        subdirectory: Option<&'data Path>,
 | 
					        subdirectory: Option<&'data Path>,
 | 
				
			||||||
 | 
					        ext: SourceDistExtension,
 | 
				
			||||||
        hashes: HashPolicy<'_>,
 | 
					        hashes: HashPolicy<'_>,
 | 
				
			||||||
        client: &ManagedClient<'_>,
 | 
					        client: &ManagedClient<'_>,
 | 
				
			||||||
    ) -> Result<ArchiveMetadata, Error> {
 | 
					    ) -> Result<ArchiveMetadata, Error> {
 | 
				
			||||||
| 
						 | 
					@ -519,7 +523,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Fetch the revision for the source distribution.
 | 
					        // Fetch the revision for the source distribution.
 | 
				
			||||||
        let revision = self
 | 
					        let revision = self
 | 
				
			||||||
            .url_revision(source, filename, url, cache_shard, hashes, client)
 | 
					            .url_revision(source, filename, ext, url, cache_shard, hashes, client)
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Before running the build, check that the hashes match.
 | 
					        // Before running the build, check that the hashes match.
 | 
				
			||||||
| 
						 | 
					@ -600,6 +604,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        &self,
 | 
					        &self,
 | 
				
			||||||
        source: &BuildableSource<'_>,
 | 
					        source: &BuildableSource<'_>,
 | 
				
			||||||
        filename: &str,
 | 
					        filename: &str,
 | 
				
			||||||
 | 
					        ext: SourceDistExtension,
 | 
				
			||||||
        url: &Url,
 | 
					        url: &Url,
 | 
				
			||||||
        cache_shard: &CacheShard,
 | 
					        cache_shard: &CacheShard,
 | 
				
			||||||
        hashes: HashPolicy<'_>,
 | 
					        hashes: HashPolicy<'_>,
 | 
				
			||||||
| 
						 | 
					@ -626,7 +631,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
                debug!("Downloading source distribution: {source}");
 | 
					                debug!("Downloading source distribution: {source}");
 | 
				
			||||||
                let entry = cache_shard.shard(revision.id()).entry(filename);
 | 
					                let entry = cache_shard.shard(revision.id()).entry(filename);
 | 
				
			||||||
                let hashes = self
 | 
					                let hashes = self
 | 
				
			||||||
                    .download_archive(response, source, filename, entry.path(), hashes)
 | 
					                    .download_archive(response, source, filename, ext, entry.path(), hashes)
 | 
				
			||||||
                    .await?;
 | 
					                    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Ok(revision.with_hashes(hashes))
 | 
					                Ok(revision.with_hashes(hashes))
 | 
				
			||||||
| 
						 | 
					@ -859,7 +864,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        debug!("Unpacking source distribution: {source}");
 | 
					        debug!("Unpacking source distribution: {source}");
 | 
				
			||||||
        let entry = cache_shard.shard(revision.id()).entry("source");
 | 
					        let entry = cache_shard.shard(revision.id()).entry("source");
 | 
				
			||||||
        let hashes = self
 | 
					        let hashes = self
 | 
				
			||||||
            .persist_archive(&resource.path, entry.path(), hashes)
 | 
					            .persist_archive(&resource.path, resource.ext, entry.path(), hashes)
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
        let revision = revision.with_hashes(hashes);
 | 
					        let revision = revision.with_hashes(hashes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1306,6 +1311,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        response: Response,
 | 
					        response: Response,
 | 
				
			||||||
        source: &BuildableSource<'_>,
 | 
					        source: &BuildableSource<'_>,
 | 
				
			||||||
        filename: &str,
 | 
					        filename: &str,
 | 
				
			||||||
 | 
					        ext: SourceDistExtension,
 | 
				
			||||||
        target: &Path,
 | 
					        target: &Path,
 | 
				
			||||||
        hashes: HashPolicy<'_>,
 | 
					        hashes: HashPolicy<'_>,
 | 
				
			||||||
    ) -> Result<Vec<HashDigest>, Error> {
 | 
					    ) -> Result<Vec<HashDigest>, Error> {
 | 
				
			||||||
| 
						 | 
					@ -1327,7 +1333,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Download and unzip the source distribution into a temporary directory.
 | 
					        // Download and unzip the source distribution into a temporary directory.
 | 
				
			||||||
        let span = info_span!("download_source_dist", filename = filename, source_dist = %source);
 | 
					        let span = info_span!("download_source_dist", filename = filename, source_dist = %source);
 | 
				
			||||||
        uv_extract::stream::archive(&mut hasher, filename, temp_dir.path()).await?;
 | 
					        uv_extract::stream::archive(&mut hasher, ext, temp_dir.path()).await?;
 | 
				
			||||||
        drop(span);
 | 
					        drop(span);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // If necessary, exhaust the reader to compute the hash.
 | 
					        // If necessary, exhaust the reader to compute the hash.
 | 
				
			||||||
| 
						 | 
					@ -1359,6 +1365,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
    async fn persist_archive(
 | 
					    async fn persist_archive(
 | 
				
			||||||
        &self,
 | 
					        &self,
 | 
				
			||||||
        path: &Path,
 | 
					        path: &Path,
 | 
				
			||||||
 | 
					        ext: SourceDistExtension,
 | 
				
			||||||
        target: &Path,
 | 
					        target: &Path,
 | 
				
			||||||
        hashes: HashPolicy<'_>,
 | 
					        hashes: HashPolicy<'_>,
 | 
				
			||||||
    ) -> Result<Vec<HashDigest>, Error> {
 | 
					    ) -> Result<Vec<HashDigest>, Error> {
 | 
				
			||||||
| 
						 | 
					@ -1380,7 +1387,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
 | 
				
			||||||
        let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
 | 
					        let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Unzip the archive into a temporary directory.
 | 
					        // Unzip the archive into a temporary directory.
 | 
				
			||||||
        uv_extract::stream::archive(&mut hasher, path, &temp_dir.path()).await?;
 | 
					        uv_extract::stream::archive(&mut hasher, ext, &temp_dir.path()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // If necessary, exhaust the reader to compute the hash.
 | 
					        // If necessary, exhaust the reader to compute the hash.
 | 
				
			||||||
        if !hashes.is_none() {
 | 
					        if !hashes.is_none() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ license = { workspace = true }
 | 
				
			||||||
workspace = true
 | 
					workspace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					distribution-filename = { workspace = true }
 | 
				
			||||||
pypi-types = { workspace = true }
 | 
					pypi-types = { workspace = true }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] }
 | 
					async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,13 @@
 | 
				
			||||||
use std::path::Path;
 | 
					use std::path::Path;
 | 
				
			||||||
use std::pin::Pin;
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::Error;
 | 
				
			||||||
 | 
					use distribution_filename::SourceDistExtension;
 | 
				
			||||||
use futures::StreamExt;
 | 
					use futures::StreamExt;
 | 
				
			||||||
use rustc_hash::FxHashSet;
 | 
					use rustc_hash::FxHashSet;
 | 
				
			||||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
 | 
					use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
 | 
				
			||||||
use tracing::warn;
 | 
					use tracing::warn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::Error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const DEFAULT_BUF_SIZE: usize = 128 * 1024;
 | 
					const DEFAULT_BUF_SIZE: usize = 128 * 1024;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Unzip a `.zip` archive into the target directory, without requiring `Seek`.
 | 
					/// Unzip a `.zip` archive into the target directory, without requiring `Seek`.
 | 
				
			||||||
| 
						 | 
					@ -231,77 +231,25 @@ pub async fn untar_xz<R: tokio::io::AsyncRead + Unpin>(
 | 
				
			||||||
/// without requiring `Seek`.
 | 
					/// without requiring `Seek`.
 | 
				
			||||||
pub async fn archive<R: tokio::io::AsyncRead + Unpin>(
 | 
					pub async fn archive<R: tokio::io::AsyncRead + Unpin>(
 | 
				
			||||||
    reader: R,
 | 
					    reader: R,
 | 
				
			||||||
    source: impl AsRef<Path>,
 | 
					    ext: SourceDistExtension,
 | 
				
			||||||
    target: impl AsRef<Path>,
 | 
					    target: impl AsRef<Path>,
 | 
				
			||||||
) -> Result<(), Error> {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    // `.zip`
 | 
					    match ext {
 | 
				
			||||||
    if source
 | 
					        SourceDistExtension::Zip => {
 | 
				
			||||||
        .as_ref()
 | 
					 | 
				
			||||||
        .extension()
 | 
					 | 
				
			||||||
        .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
            unzip(reader, target).await?;
 | 
					            unzip(reader, target).await?;
 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        SourceDistExtension::TarGz => {
 | 
				
			||||||
    // `.tar.gz`
 | 
					 | 
				
			||||||
    if source
 | 
					 | 
				
			||||||
        .as_ref()
 | 
					 | 
				
			||||||
        .extension()
 | 
					 | 
				
			||||||
        .is_some_and(|ext| ext.eq_ignore_ascii_case("gz"))
 | 
					 | 
				
			||||||
        && source.as_ref().file_stem().is_some_and(|stem| {
 | 
					 | 
				
			||||||
            Path::new(stem)
 | 
					 | 
				
			||||||
                .extension()
 | 
					 | 
				
			||||||
                .is_some_and(|ext| ext.eq_ignore_ascii_case("tar"))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
            untar_gz(reader, target).await?;
 | 
					            untar_gz(reader, target).await?;
 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        SourceDistExtension::TarBz2 => {
 | 
				
			||||||
    // `.tar.bz2`
 | 
					 | 
				
			||||||
    if source
 | 
					 | 
				
			||||||
        .as_ref()
 | 
					 | 
				
			||||||
        .extension()
 | 
					 | 
				
			||||||
        .is_some_and(|ext| ext.eq_ignore_ascii_case("bz2"))
 | 
					 | 
				
			||||||
        && source.as_ref().file_stem().is_some_and(|stem| {
 | 
					 | 
				
			||||||
            Path::new(stem)
 | 
					 | 
				
			||||||
                .extension()
 | 
					 | 
				
			||||||
                .is_some_and(|ext| ext.eq_ignore_ascii_case("tar"))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
            untar_bz2(reader, target).await?;
 | 
					            untar_bz2(reader, target).await?;
 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    // `.tar.zst`
 | 
					        SourceDistExtension::TarXz => {
 | 
				
			||||||
    if source
 | 
					 | 
				
			||||||
        .as_ref()
 | 
					 | 
				
			||||||
        .extension()
 | 
					 | 
				
			||||||
        .is_some_and(|ext| ext.eq_ignore_ascii_case("zst"))
 | 
					 | 
				
			||||||
        && source.as_ref().file_stem().is_some_and(|stem| {
 | 
					 | 
				
			||||||
            Path::new(stem)
 | 
					 | 
				
			||||||
                .extension()
 | 
					 | 
				
			||||||
                .is_some_and(|ext| ext.eq_ignore_ascii_case("tar"))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        untar_zst(reader, target).await?;
 | 
					 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // `.tar.xz`
 | 
					 | 
				
			||||||
    if source
 | 
					 | 
				
			||||||
        .as_ref()
 | 
					 | 
				
			||||||
        .extension()
 | 
					 | 
				
			||||||
        .is_some_and(|ext| ext.eq_ignore_ascii_case("xz"))
 | 
					 | 
				
			||||||
        && source.as_ref().file_stem().is_some_and(|stem| {
 | 
					 | 
				
			||||||
            Path::new(stem)
 | 
					 | 
				
			||||||
                .extension()
 | 
					 | 
				
			||||||
                .is_some_and(|ext| ext.eq_ignore_ascii_case("tar"))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
            untar_xz(reader, target).await?;
 | 
					            untar_xz(reader, target).await?;
 | 
				
			||||||
        return Ok(());
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        SourceDistExtension::TarZst => {
 | 
				
			||||||
    Err(Error::UnsupportedArchive(source.as_ref().to_path_buf()))
 | 
					            untar_zst(reader, target).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,11 @@
 | 
				
			||||||
use std::collections::hash_map::Entry;
 | 
					use std::collections::hash_map::Entry;
 | 
				
			||||||
use std::path::Path;
 | 
					 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use anyhow::{bail, Result};
 | 
					use anyhow::{bail, Result};
 | 
				
			||||||
use rustc_hash::{FxBuildHasher, FxHashMap};
 | 
					use rustc_hash::{FxBuildHasher, FxHashMap};
 | 
				
			||||||
use tracing::debug;
 | 
					use tracing::debug;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use distribution_filename::WheelFilename;
 | 
					use distribution_filename::{DistExtension, WheelFilename};
 | 
				
			||||||
use distribution_types::{
 | 
					use distribution_types::{
 | 
				
			||||||
    CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
 | 
					    CachedDirectUrlDist, CachedDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
 | 
				
			||||||
    Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, Name, PathBuiltDist,
 | 
					    Error, GitSourceDist, Hashed, IndexLocations, InstalledDist, Name, PathBuiltDist,
 | 
				
			||||||
| 
						 | 
					@ -152,15 +151,13 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                RequirementSource::Url {
 | 
					                RequirementSource::Url {
 | 
				
			||||||
                    subdirectory,
 | 
					 | 
				
			||||||
                    location,
 | 
					                    location,
 | 
				
			||||||
 | 
					                    subdirectory,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                } => {
 | 
					                } => {
 | 
				
			||||||
                    // Check if we have a wheel or a source distribution.
 | 
					                    match ext {
 | 
				
			||||||
                    if Path::new(url.path())
 | 
					                        DistExtension::Wheel => {
 | 
				
			||||||
                        .extension()
 | 
					 | 
				
			||||||
                        .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                            // Validate that the name in the wheel matches that of the requirement.
 | 
					                            // Validate that the name in the wheel matches that of the requirement.
 | 
				
			||||||
                            let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
					                            let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
				
			||||||
                            if filename.name != requirement.name {
 | 
					                            if filename.name != requirement.name {
 | 
				
			||||||
| 
						 | 
					@ -217,11 +214,13 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                                    continue;
 | 
					                                    continue;
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                    } else {
 | 
					                        }
 | 
				
			||||||
 | 
					                        DistExtension::Source(ext) => {
 | 
				
			||||||
                            let sdist = DirectUrlSourceDist {
 | 
					                            let sdist = DirectUrlSourceDist {
 | 
				
			||||||
                                name: requirement.name.clone(),
 | 
					                                name: requirement.name.clone(),
 | 
				
			||||||
                                location: location.clone(),
 | 
					                                location: location.clone(),
 | 
				
			||||||
                                subdirectory: subdirectory.clone(),
 | 
					                                subdirectory: subdirectory.clone(),
 | 
				
			||||||
 | 
					                                ext: *ext,
 | 
				
			||||||
                                url: url.clone(),
 | 
					                                url: url.clone(),
 | 
				
			||||||
                            };
 | 
					                            };
 | 
				
			||||||
                            // Find the most-compatible wheel from the cache, since we don't know
 | 
					                            // Find the most-compatible wheel from the cache, since we don't know
 | 
				
			||||||
| 
						 | 
					@ -234,6 +233,7 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                RequirementSource::Git {
 | 
					                RequirementSource::Git {
 | 
				
			||||||
                    repository,
 | 
					                    repository,
 | 
				
			||||||
                    reference,
 | 
					                    reference,
 | 
				
			||||||
| 
						 | 
					@ -300,6 +300,7 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                RequirementSource::Path {
 | 
					                RequirementSource::Path {
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                    install_path,
 | 
					                    install_path,
 | 
				
			||||||
                    lock_path,
 | 
					                    lock_path,
 | 
				
			||||||
| 
						 | 
					@ -313,11 +314,8 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                        Err(err) => return Err(err.into()),
 | 
					                        Err(err) => return Err(err.into()),
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Check if we have a wheel or a source distribution.
 | 
					                    match ext {
 | 
				
			||||||
                    if path
 | 
					                        DistExtension::Wheel => {
 | 
				
			||||||
                        .extension()
 | 
					 | 
				
			||||||
                        .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                            // Validate that the name in the wheel matches that of the requirement.
 | 
					                            // Validate that the name in the wheel matches that of the requirement.
 | 
				
			||||||
                            let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
					                            let filename = WheelFilename::from_str(&url.filename()?)?;
 | 
				
			||||||
                            if filename.name != requirement.name {
 | 
					                            if filename.name != requirement.name {
 | 
				
			||||||
| 
						 | 
					@ -370,18 +368,22 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                                            cache.archive(&archive.id),
 | 
					                                            cache.archive(&archive.id),
 | 
				
			||||||
                                        );
 | 
					                                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                    debug!("Path wheel requirement already cached: {cached_dist}");
 | 
					                                        debug!(
 | 
				
			||||||
 | 
					                                            "Path wheel requirement already cached: {cached_dist}"
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
                                        cached.push(CachedDist::Url(cached_dist));
 | 
					                                        cached.push(CachedDist::Url(cached_dist));
 | 
				
			||||||
                                        continue;
 | 
					                                        continue;
 | 
				
			||||||
                                    }
 | 
					                                    }
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                    } else {
 | 
					                        }
 | 
				
			||||||
 | 
					                        DistExtension::Source(ext) => {
 | 
				
			||||||
                            let sdist = PathSourceDist {
 | 
					                            let sdist = PathSourceDist {
 | 
				
			||||||
                                name: requirement.name.clone(),
 | 
					                                name: requirement.name.clone(),
 | 
				
			||||||
                                url: url.clone(),
 | 
					                                url: url.clone(),
 | 
				
			||||||
                                install_path: path,
 | 
					                                install_path: path,
 | 
				
			||||||
                                lock_path: lock_path.clone(),
 | 
					                                lock_path: lock_path.clone(),
 | 
				
			||||||
 | 
					                                ext: *ext,
 | 
				
			||||||
                            };
 | 
					                            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            // Find the most-compatible wheel from the cache, since we don't know
 | 
					                            // Find the most-compatible wheel from the cache, since we don't know
 | 
				
			||||||
| 
						 | 
					@ -395,6 +397,7 @@ impl<'a> Planner<'a> {
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            debug!("Identified uncached requirement: {requirement}");
 | 
					            debug!("Identified uncached requirement: {requirement}");
 | 
				
			||||||
            remote.push(requirement.clone());
 | 
					            remote.push(requirement.clone());
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,7 @@ impl RequirementSatisfaction {
 | 
				
			||||||
                // records `"url": "https://github.com/tqdm/tqdm"` in `direct_url.json`.
 | 
					                // records `"url": "https://github.com/tqdm/tqdm"` in `direct_url.json`.
 | 
				
			||||||
                location: requested_url,
 | 
					                location: requested_url,
 | 
				
			||||||
                subdirectory: requested_subdirectory,
 | 
					                subdirectory: requested_subdirectory,
 | 
				
			||||||
 | 
					                ext: _,
 | 
				
			||||||
                url: _,
 | 
					                url: _,
 | 
				
			||||||
            } => {
 | 
					            } => {
 | 
				
			||||||
                let InstalledDist::Url(InstalledDirectUrlDist {
 | 
					                let InstalledDist::Url(InstalledDirectUrlDist {
 | 
				
			||||||
| 
						 | 
					@ -150,6 +151,7 @@ impl RequirementSatisfaction {
 | 
				
			||||||
            RequirementSource::Path {
 | 
					            RequirementSource::Path {
 | 
				
			||||||
                install_path: requested_path,
 | 
					                install_path: requested_path,
 | 
				
			||||||
                lock_path: _,
 | 
					                lock_path: _,
 | 
				
			||||||
 | 
					                ext: _,
 | 
				
			||||||
                url: _,
 | 
					                url: _,
 | 
				
			||||||
            } => {
 | 
					            } => {
 | 
				
			||||||
                let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
 | 
					                let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,14 +14,15 @@ workspace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
cache-key = { workspace = true }
 | 
					cache-key = { workspace = true }
 | 
				
			||||||
 | 
					distribution-filename = { workspace = true }
 | 
				
			||||||
install-wheel-rs = { workspace = true }
 | 
					install-wheel-rs = { workspace = true }
 | 
				
			||||||
pep440_rs = { workspace = true }
 | 
					pep440_rs = { workspace = true }
 | 
				
			||||||
pep508_rs = { workspace = true }
 | 
					pep508_rs = { workspace = true }
 | 
				
			||||||
platform-tags = { workspace = true }
 | 
					platform-tags = { workspace = true }
 | 
				
			||||||
pypi-types = { workspace = true }
 | 
					pypi-types = { workspace = true }
 | 
				
			||||||
uv-cache = { workspace = true }
 | 
					uv-cache = { workspace = true }
 | 
				
			||||||
uv-configuration = { workspace = true }
 | 
					 | 
				
			||||||
uv-client = { workspace = true }
 | 
					uv-client = { workspace = true }
 | 
				
			||||||
 | 
					uv-configuration = { workspace = true }
 | 
				
			||||||
uv-extract = { workspace = true }
 | 
					uv-extract = { workspace = true }
 | 
				
			||||||
uv-fs = { workspace = true }
 | 
					uv-fs = { workspace = true }
 | 
				
			||||||
uv-state = { workspace = true }
 | 
					uv-state = { workspace = true }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,15 +5,15 @@ use std::pin::Pin;
 | 
				
			||||||
use std::str::FromStr;
 | 
					use std::str::FromStr;
 | 
				
			||||||
use std::task::{Context, Poll};
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use distribution_filename::{ExtensionError, SourceDistExtension};
 | 
				
			||||||
use futures::TryStreamExt;
 | 
					use futures::TryStreamExt;
 | 
				
			||||||
use owo_colors::OwoColorize;
 | 
					use owo_colors::OwoColorize;
 | 
				
			||||||
 | 
					use pypi_types::{HashAlgorithm, HashDigest};
 | 
				
			||||||
use thiserror::Error;
 | 
					use thiserror::Error;
 | 
				
			||||||
use tokio::io::{AsyncRead, ReadBuf};
 | 
					use tokio::io::{AsyncRead, ReadBuf};
 | 
				
			||||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
 | 
					use tokio_util::compat::FuturesAsyncReadCompatExt;
 | 
				
			||||||
use tracing::{debug, instrument};
 | 
					use tracing::{debug, instrument};
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use pypi_types::{HashAlgorithm, HashDigest};
 | 
					 | 
				
			||||||
use uv_cache::Cache;
 | 
					use uv_cache::Cache;
 | 
				
			||||||
use uv_client::WrappedReqwestError;
 | 
					use uv_client::WrappedReqwestError;
 | 
				
			||||||
use uv_extract::hash::Hasher;
 | 
					use uv_extract::hash::Hasher;
 | 
				
			||||||
| 
						 | 
					@ -32,6 +32,8 @@ pub enum Error {
 | 
				
			||||||
    Io(#[from] io::Error),
 | 
					    Io(#[from] io::Error),
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
    ImplementationError(#[from] ImplementationError),
 | 
					    ImplementationError(#[from] ImplementationError),
 | 
				
			||||||
 | 
					    #[error("Expected download URL (`{0}`) to end in a supported file extension: {1}")]
 | 
				
			||||||
 | 
					    MissingExtension(String, ExtensionError),
 | 
				
			||||||
    #[error("Invalid Python version: {0}")]
 | 
					    #[error("Invalid Python version: {0}")]
 | 
				
			||||||
    InvalidPythonVersion(String),
 | 
					    InvalidPythonVersion(String),
 | 
				
			||||||
    #[error("Invalid request key (too many parts): {0}")]
 | 
					    #[error("Invalid request key (too many parts): {0}")]
 | 
				
			||||||
| 
						 | 
					@ -423,6 +425,8 @@ impl ManagedPythonDownload {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let filename = url.path_segments().unwrap().last().unwrap();
 | 
					        let filename = url.path_segments().unwrap().last().unwrap();
 | 
				
			||||||
 | 
					        let ext = SourceDistExtension::from_path(filename)
 | 
				
			||||||
 | 
					            .map_err(|err| Error::MissingExtension(url.to_string(), err))?;
 | 
				
			||||||
        let response = client.get(url.clone()).send().await?;
 | 
					        let response = client.get(url.clone()).send().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Ensure the request was successful.
 | 
					        // Ensure the request was successful.
 | 
				
			||||||
| 
						 | 
					@ -458,12 +462,12 @@ impl ManagedPythonDownload {
 | 
				
			||||||
        match progress {
 | 
					        match progress {
 | 
				
			||||||
            Some((&reporter, progress)) => {
 | 
					            Some((&reporter, progress)) => {
 | 
				
			||||||
                let mut reader = ProgressReader::new(&mut hasher, progress, reporter);
 | 
					                let mut reader = ProgressReader::new(&mut hasher, progress, reporter);
 | 
				
			||||||
                uv_extract::stream::archive(&mut reader, filename, temp_dir.path())
 | 
					                uv_extract::stream::archive(&mut reader, ext, temp_dir.path())
 | 
				
			||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .map_err(|err| Error::ExtractError(filename.to_string(), err))?;
 | 
					                    .map_err(|err| Error::ExtractError(filename.to_string(), err))?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            None => {
 | 
					            None => {
 | 
				
			||||||
                uv_extract::stream::archive(&mut hasher, filename, temp_dir.path())
 | 
					                uv_extract::stream::archive(&mut hasher, ext, temp_dir.path())
 | 
				
			||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .map_err(|err| Error::ExtractError(filename.to_string(), err))?;
 | 
					                    .map_err(|err| Error::ExtractError(filename.to_string(), err))?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -247,12 +247,14 @@ fn required_dist(requirement: &Requirement) -> Result<Option<Dist>, distribution
 | 
				
			||||||
        RequirementSource::Url {
 | 
					        RequirementSource::Url {
 | 
				
			||||||
            subdirectory,
 | 
					            subdirectory,
 | 
				
			||||||
            location,
 | 
					            location,
 | 
				
			||||||
 | 
					            ext,
 | 
				
			||||||
            url,
 | 
					            url,
 | 
				
			||||||
        } => Dist::from_http_url(
 | 
					        } => Dist::from_http_url(
 | 
				
			||||||
            requirement.name.clone(),
 | 
					            requirement.name.clone(),
 | 
				
			||||||
            url.clone(),
 | 
					            url.clone(),
 | 
				
			||||||
            location.clone(),
 | 
					            location.clone(),
 | 
				
			||||||
            subdirectory.clone(),
 | 
					            subdirectory.clone(),
 | 
				
			||||||
 | 
					            *ext,
 | 
				
			||||||
        )?,
 | 
					        )?,
 | 
				
			||||||
        RequirementSource::Git {
 | 
					        RequirementSource::Git {
 | 
				
			||||||
            repository,
 | 
					            repository,
 | 
				
			||||||
| 
						 | 
					@ -276,12 +278,14 @@ fn required_dist(requirement: &Requirement) -> Result<Option<Dist>, distribution
 | 
				
			||||||
        RequirementSource::Path {
 | 
					        RequirementSource::Path {
 | 
				
			||||||
            install_path,
 | 
					            install_path,
 | 
				
			||||||
            lock_path,
 | 
					            lock_path,
 | 
				
			||||||
 | 
					            ext,
 | 
				
			||||||
            url,
 | 
					            url,
 | 
				
			||||||
        } => Dist::from_file_url(
 | 
					        } => Dist::from_file_url(
 | 
				
			||||||
            requirement.name.clone(),
 | 
					            requirement.name.clone(),
 | 
				
			||||||
            url.clone(),
 | 
					            url.clone(),
 | 
				
			||||||
            install_path,
 | 
					            install_path,
 | 
				
			||||||
            lock_path,
 | 
					            lock_path,
 | 
				
			||||||
 | 
					            *ext,
 | 
				
			||||||
        )?,
 | 
					        )?,
 | 
				
			||||||
        RequirementSource::Directory {
 | 
					        RequirementSource::Directory {
 | 
				
			||||||
            install_path,
 | 
					            install_path,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ use serde::Deserialize;
 | 
				
			||||||
use tracing::debug;
 | 
					use tracing::debug;
 | 
				
			||||||
use url::Host;
 | 
					use url::Host;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use distribution_filename::{SourceDistFilename, WheelFilename};
 | 
					use distribution_filename::{DistExtension, SourceDistFilename, WheelFilename};
 | 
				
			||||||
use distribution_types::{
 | 
					use distribution_types::{
 | 
				
			||||||
    BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl,
 | 
					    BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl,
 | 
				
			||||||
    RemoteSource, SourceUrl, UnresolvedRequirement, UnresolvedRequirementSpecification, VersionId,
 | 
					    RemoteSource, SourceUrl, UnresolvedRequirement, UnresolvedRequirementSpecification, VersionId,
 | 
				
			||||||
| 
						 | 
					@ -260,13 +260,28 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
 | 
				
			||||||
                    editable: parsed_directory_url.editable,
 | 
					                    editable: parsed_directory_url.editable,
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ParsedUrl::Path(parsed_path_url) => SourceUrl::Path(PathSourceUrl {
 | 
					            ParsedUrl::Path(parsed_path_url) => {
 | 
				
			||||||
 | 
					                let ext = match parsed_path_url.ext {
 | 
				
			||||||
 | 
					                    DistExtension::Source(ext) => ext,
 | 
				
			||||||
 | 
					                    DistExtension::Wheel => unreachable!(),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                SourceUrl::Path(PathSourceUrl {
 | 
				
			||||||
                    url: &requirement.url.verbatim,
 | 
					                    url: &requirement.url.verbatim,
 | 
				
			||||||
                    path: Cow::Borrowed(&parsed_path_url.install_path),
 | 
					                    path: Cow::Borrowed(&parsed_path_url.install_path),
 | 
				
			||||||
            }),
 | 
					                    ext,
 | 
				
			||||||
            ParsedUrl::Archive(parsed_archive_url) => SourceUrl::Direct(DirectSourceUrl {
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ParsedUrl::Archive(parsed_archive_url) => {
 | 
				
			||||||
 | 
					                let ext = match parsed_archive_url.ext {
 | 
				
			||||||
 | 
					                    DistExtension::Source(ext) => ext,
 | 
				
			||||||
 | 
					                    DistExtension::Wheel => unreachable!(),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                SourceUrl::Direct(DirectSourceUrl {
 | 
				
			||||||
                    url: &parsed_archive_url.url,
 | 
					                    url: &parsed_archive_url.url,
 | 
				
			||||||
            }),
 | 
					                    subdirectory: parsed_archive_url.subdirectory.as_deref(),
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            ParsedUrl::Git(parsed_git_url) => SourceUrl::Git(GitSourceUrl {
 | 
					            ParsedUrl::Git(parsed_git_url) => SourceUrl::Git(GitSourceUrl {
 | 
				
			||||||
                url: &requirement.url.verbatim,
 | 
					                url: &requirement.url.verbatim,
 | 
				
			||||||
                git: &parsed_git_url.url,
 | 
					                git: &parsed_git_url.url,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +65,9 @@ pub enum ResolveError {
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error(transparent)]
 | 
				
			||||||
    DistributionType(#[from] distribution_types::Error),
 | 
					    DistributionType(#[from] distribution_types::Error),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error(transparent)]
 | 
				
			||||||
 | 
					    ParsedUrl(#[from] pypi_types::ParsedUrlError),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[error("Failed to download `{0}`")]
 | 
					    #[error("Failed to download `{0}`")]
 | 
				
			||||||
    Fetch(Box<BuiltDist>, #[source] uv_distribution::Error),
 | 
					    Fetch(Box<BuiltDist>, #[source] uv_distribution::Error),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,6 +96,7 @@ impl FlatIndex {
 | 
				
			||||||
                let dist = RegistrySourceDist {
 | 
					                let dist = RegistrySourceDist {
 | 
				
			||||||
                    name: filename.name.clone(),
 | 
					                    name: filename.name.clone(),
 | 
				
			||||||
                    version: filename.version.clone(),
 | 
					                    version: filename.version.clone(),
 | 
				
			||||||
 | 
					                    ext: filename.extension,
 | 
				
			||||||
                    file: Box::new(file),
 | 
					                    file: Box::new(file),
 | 
				
			||||||
                    index,
 | 
					                    index,
 | 
				
			||||||
                    wheels: vec![],
 | 
					                    wheels: vec![],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use cache_key::RepositoryUrl;
 | 
					use cache_key::RepositoryUrl;
 | 
				
			||||||
use distribution_filename::WheelFilename;
 | 
					use distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename};
 | 
				
			||||||
use distribution_types::{
 | 
					use distribution_types::{
 | 
				
			||||||
    BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist,
 | 
					    BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist,
 | 
				
			||||||
    DistributionMetadata, FileLocation, GitSourceDist, HashComparison, IndexUrl, Name,
 | 
					    DistributionMetadata, FileLocation, GitSourceDist, HashComparison, IndexUrl, Name,
 | 
				
			||||||
| 
						 | 
					@ -791,6 +791,7 @@ impl Package {
 | 
				
			||||||
                    let url = Url::from(ParsedArchiveUrl {
 | 
					                    let url = Url::from(ParsedArchiveUrl {
 | 
				
			||||||
                        url: url.to_url(),
 | 
					                        url: url.to_url(),
 | 
				
			||||||
                        subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
					                        subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
				
			||||||
 | 
					                        ext: DistExtension::Wheel,
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                    let direct_dist = DirectUrlBuiltDist {
 | 
					                    let direct_dist = DirectUrlBuiltDist {
 | 
				
			||||||
                        filename,
 | 
					                        filename,
 | 
				
			||||||
| 
						 | 
					@ -843,6 +844,7 @@ impl Package {
 | 
				
			||||||
                    url: verbatim_url(workspace_root.join(path), &self.id)?,
 | 
					                    url: verbatim_url(workspace_root.join(path), &self.id)?,
 | 
				
			||||||
                    install_path: workspace_root.join(path),
 | 
					                    install_path: workspace_root.join(path),
 | 
				
			||||||
                    lock_path: path.clone(),
 | 
					                    lock_path: path.clone(),
 | 
				
			||||||
 | 
					                    ext: SourceDistExtension::from_path(path)?,
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
                distribution_types::SourceDist::Path(path_dist)
 | 
					                distribution_types::SourceDist::Path(path_dist)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -895,14 +897,18 @@ impl Package {
 | 
				
			||||||
                distribution_types::SourceDist::Git(git_dist)
 | 
					                distribution_types::SourceDist::Git(git_dist)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            Source::Direct(url, direct) => {
 | 
					            Source::Direct(url, direct) => {
 | 
				
			||||||
 | 
					                let ext = SourceDistExtension::from_path(url.as_ref())?;
 | 
				
			||||||
 | 
					                let subdirectory = direct.subdirectory.as_ref().map(PathBuf::from);
 | 
				
			||||||
                let url = Url::from(ParsedArchiveUrl {
 | 
					                let url = Url::from(ParsedArchiveUrl {
 | 
				
			||||||
                    url: url.to_url(),
 | 
					                    url: url.to_url(),
 | 
				
			||||||
                    subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
					                    subdirectory: subdirectory.clone(),
 | 
				
			||||||
 | 
					                    ext: DistExtension::Source(ext),
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                let direct_dist = DirectUrlSourceDist {
 | 
					                let direct_dist = DirectUrlSourceDist {
 | 
				
			||||||
                    name: self.id.name.clone(),
 | 
					                    name: self.id.name.clone(),
 | 
				
			||||||
                    location: url.clone(),
 | 
					                    location: url.clone(),
 | 
				
			||||||
                    subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
					                    subdirectory: subdirectory.clone(),
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    url: VerbatimUrl::from_url(url),
 | 
					                    url: VerbatimUrl::from_url(url),
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
                distribution_types::SourceDist::DirectUrl(direct_dist)
 | 
					                distribution_types::SourceDist::DirectUrl(direct_dist)
 | 
				
			||||||
| 
						 | 
					@ -920,6 +926,7 @@ impl Package {
 | 
				
			||||||
                    .ok_or_else(|| LockErrorKind::MissingFilename {
 | 
					                    .ok_or_else(|| LockErrorKind::MissingFilename {
 | 
				
			||||||
                        id: self.id.clone(),
 | 
					                        id: self.id.clone(),
 | 
				
			||||||
                    })?;
 | 
					                    })?;
 | 
				
			||||||
 | 
					                let ext = SourceDistExtension::from_path(filename.as_ref())?;
 | 
				
			||||||
                let file = Box::new(distribution_types::File {
 | 
					                let file = Box::new(distribution_types::File {
 | 
				
			||||||
                    dist_info_metadata: false,
 | 
					                    dist_info_metadata: false,
 | 
				
			||||||
                    filename: filename.to_string(),
 | 
					                    filename: filename.to_string(),
 | 
				
			||||||
| 
						 | 
					@ -939,6 +946,7 @@ impl Package {
 | 
				
			||||||
                    name: self.id.name.clone(),
 | 
					                    name: self.id.name.clone(),
 | 
				
			||||||
                    version: self.id.version.clone(),
 | 
					                    version: self.id.version.clone(),
 | 
				
			||||||
                    file,
 | 
					                    file,
 | 
				
			||||||
 | 
					                    ext,
 | 
				
			||||||
                    index,
 | 
					                    index,
 | 
				
			||||||
                    wheels: vec![],
 | 
					                    wheels: vec![],
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
| 
						 | 
					@ -2232,6 +2240,7 @@ impl Dependency {
 | 
				
			||||||
                let parsed_url = ParsedUrl::Archive(ParsedArchiveUrl {
 | 
					                let parsed_url = ParsedUrl::Archive(ParsedArchiveUrl {
 | 
				
			||||||
                    url: url.to_url(),
 | 
					                    url: url.to_url(),
 | 
				
			||||||
                    subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
					                    subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
 | 
				
			||||||
 | 
					                    ext: DistExtension::from_path(url.as_ref())?,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                RequirementSource::from_verbatim_parsed_url(parsed_url)
 | 
					                RequirementSource::from_verbatim_parsed_url(parsed_url)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -2239,6 +2248,7 @@ impl Dependency {
 | 
				
			||||||
                lock_path: path.clone(),
 | 
					                lock_path: path.clone(),
 | 
				
			||||||
                install_path: workspace_root.join(path),
 | 
					                install_path: workspace_root.join(path),
 | 
				
			||||||
                url: verbatim_url(workspace_root.join(path), &self.package_id)?,
 | 
					                url: verbatim_url(workspace_root.join(path), &self.package_id)?,
 | 
				
			||||||
 | 
					                ext: DistExtension::from_path(path)?,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            Source::Directory(ref path) => RequirementSource::Directory {
 | 
					            Source::Directory(ref path) => RequirementSource::Directory {
 | 
				
			||||||
                editable: false,
 | 
					                editable: false,
 | 
				
			||||||
| 
						 | 
					@ -2459,6 +2469,10 @@ enum LockErrorKind {
 | 
				
			||||||
        #[source]
 | 
					        #[source]
 | 
				
			||||||
        ToUrlError,
 | 
					        ToUrlError,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    /// An error that occurs when the extension can't be determined
 | 
				
			||||||
 | 
					    /// for a given wheel or source distribution.
 | 
				
			||||||
 | 
					    #[error("failed to parse file extension; expected one of: {0}")]
 | 
				
			||||||
 | 
					    MissingExtension(#[from] ExtensionError),
 | 
				
			||||||
    /// Failed to parse a git source URL.
 | 
					    /// Failed to parse a git source URL.
 | 
				
			||||||
    #[error("failed to parse source git URL")]
 | 
					    #[error("failed to parse source git URL")]
 | 
				
			||||||
    InvalidGitSourceUrl(
 | 
					    InvalidGitSourceUrl(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,11 +106,13 @@ impl PubGrubRequirement {
 | 
				
			||||||
            RequirementSource::Url {
 | 
					            RequirementSource::Url {
 | 
				
			||||||
                subdirectory,
 | 
					                subdirectory,
 | 
				
			||||||
                location,
 | 
					                location,
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
            } => {
 | 
					            } => {
 | 
				
			||||||
                let parsed_url = ParsedUrl::Archive(ParsedArchiveUrl::from_source(
 | 
					                let parsed_url = ParsedUrl::Archive(ParsedArchiveUrl::from_source(
 | 
				
			||||||
                    location.clone(),
 | 
					                    location.clone(),
 | 
				
			||||||
                    subdirectory.clone(),
 | 
					                    subdirectory.clone(),
 | 
				
			||||||
 | 
					                    *ext,
 | 
				
			||||||
                ));
 | 
					                ));
 | 
				
			||||||
                (url, parsed_url)
 | 
					                (url, parsed_url)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -130,6 +132,7 @@ impl PubGrubRequirement {
 | 
				
			||||||
                (url, parsed_url)
 | 
					                (url, parsed_url)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            RequirementSource::Path {
 | 
					            RequirementSource::Path {
 | 
				
			||||||
 | 
					                ext,
 | 
				
			||||||
                url,
 | 
					                url,
 | 
				
			||||||
                install_path,
 | 
					                install_path,
 | 
				
			||||||
                lock_path,
 | 
					                lock_path,
 | 
				
			||||||
| 
						 | 
					@ -137,6 +140,7 @@ impl PubGrubRequirement {
 | 
				
			||||||
                let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source(
 | 
					                let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source(
 | 
				
			||||||
                    install_path.clone(),
 | 
					                    install_path.clone(),
 | 
				
			||||||
                    lock_path.clone(),
 | 
					                    lock_path.clone(),
 | 
				
			||||||
 | 
					                    *ext,
 | 
				
			||||||
                    url.to_url(),
 | 
					                    url.to_url(),
 | 
				
			||||||
                ));
 | 
					                ));
 | 
				
			||||||
                (url, parsed_url)
 | 
					                (url, parsed_url)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -391,6 +391,7 @@ impl VersionMapLazy {
 | 
				
			||||||
                        let dist = RegistrySourceDist {
 | 
					                        let dist = RegistrySourceDist {
 | 
				
			||||||
                            name: filename.name.clone(),
 | 
					                            name: filename.name.clone(),
 | 
				
			||||||
                            version: filename.version.clone(),
 | 
					                            version: filename.version.clone(),
 | 
				
			||||||
 | 
					                            ext: filename.extension,
 | 
				
			||||||
                            file: Box::new(file),
 | 
					                            file: Box::new(file),
 | 
				
			||||||
                            index: self.index.clone(),
 | 
					                            index: self.index.clone(),
 | 
				
			||||||
                            wheels: vec![],
 | 
					                            wheels: vec![],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ fn clean_package_pypi() -> Result<()> {
 | 
				
			||||||
    // Assert that the `.rkyv` file is created for `iniconfig`.
 | 
					    // Assert that the `.rkyv` file is created for `iniconfig`.
 | 
				
			||||||
    let rkyv = context
 | 
					    let rkyv = context
 | 
				
			||||||
        .cache_dir
 | 
					        .cache_dir
 | 
				
			||||||
        .child("simple-v11")
 | 
					        .child("simple-v12")
 | 
				
			||||||
        .child("pypi")
 | 
					        .child("pypi")
 | 
				
			||||||
        .child("iniconfig.rkyv");
 | 
					        .child("iniconfig.rkyv");
 | 
				
			||||||
    assert!(
 | 
					    assert!(
 | 
				
			||||||
| 
						 | 
					@ -104,7 +104,7 @@ fn clean_package_index() -> Result<()> {
 | 
				
			||||||
    // Assert that the `.rkyv` file is created for `iniconfig`.
 | 
					    // Assert that the `.rkyv` file is created for `iniconfig`.
 | 
				
			||||||
    let rkyv = context
 | 
					    let rkyv = context
 | 
				
			||||||
        .cache_dir
 | 
					        .cache_dir
 | 
				
			||||||
        .child("simple-v11")
 | 
					        .child("simple-v12")
 | 
				
			||||||
        .child("index")
 | 
					        .child("index")
 | 
				
			||||||
        .child("e8208120cae3ba69")
 | 
					        .child("e8208120cae3ba69")
 | 
				
			||||||
        .child("iniconfig.rkyv");
 | 
					        .child("iniconfig.rkyv");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11482,7 +11482,7 @@ fn tool_uv_sources() -> Result<()> {
 | 
				
			||||||
            "boltons==24.0.0"
 | 
					            "boltons==24.0.0"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        dont_install_me = [
 | 
					        dont_install_me = [
 | 
				
			||||||
            "broken @ https://example.org/does/not/exist"
 | 
					            "broken @ https://example.org/does/not/exist.tar.gz"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [tool.uv.sources]
 | 
					        [tool.uv.sources]
 | 
				
			||||||
| 
						 | 
					@ -11540,6 +11540,66 @@ fn tool_uv_sources() -> Result<()> {
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn invalid_tool_uv_sources() -> Result<()> {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write an invalid extension on a PEP 508 URL.
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(indoc! {r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.0.0"
 | 
				
			||||||
 | 
					        dependencies = [
 | 
				
			||||||
 | 
					          "urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    "#})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.pip_compile()
 | 
				
			||||||
 | 
					        .arg("--preview")
 | 
				
			||||||
 | 
					        .arg(context.temp_dir.join("pyproject.toml")), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    error: Failed to parse metadata from built wheel
 | 
				
			||||||
 | 
					      Caused by: Expected direct URL (`https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz`) to end in a supported file extension: `.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`
 | 
				
			||||||
 | 
					    urllib3 @ https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz
 | 
				
			||||||
 | 
					              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					    "###
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write an invalid extension on a source.
 | 
				
			||||||
 | 
					    let pyproject_toml = context.temp_dir.child("pyproject.toml");
 | 
				
			||||||
 | 
					    pyproject_toml.write_str(indoc! {r#"
 | 
				
			||||||
 | 
					        [project]
 | 
				
			||||||
 | 
					        name = "project"
 | 
				
			||||||
 | 
					        version = "0.0.0"
 | 
				
			||||||
 | 
					        dependencies = [
 | 
				
			||||||
 | 
					          "urllib3",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.uv.sources]
 | 
				
			||||||
 | 
					        urllib3 = { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz" }
 | 
				
			||||||
 | 
					    "#})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.pip_compile()
 | 
				
			||||||
 | 
					        .arg("--preview")
 | 
				
			||||||
 | 
					        .arg(context.temp_dir.join("pyproject.toml")), @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    error: Failed to parse entry for: `urllib3`
 | 
				
			||||||
 | 
					      Caused by: Expected direct URL (`https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.tar.baz`) to end in a supported file extension: `.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`
 | 
				
			||||||
 | 
					    "###
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Check that a dynamic `pyproject.toml` is supported a compile input file.
 | 
					/// Check that a dynamic `pyproject.toml` is supported a compile input file.
 | 
				
			||||||
#[test]
 | 
					#[test]
 | 
				
			||||||
fn dynamic_pyproject_toml() -> Result<()> {
 | 
					fn dynamic_pyproject_toml() -> Result<()> {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5676,7 +5676,7 @@ fn tool_uv_sources() -> Result<()> {
 | 
				
			||||||
            "boltons==24.0.0"
 | 
					            "boltons==24.0.0"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        dont_install_me = [
 | 
					        dont_install_me = [
 | 
				
			||||||
            "broken @ https://example.org/does/not/exist"
 | 
					            "broken @ https://example.org/does/not/exist.tar.gz"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [tool.uv.sources]
 | 
					        [tool.uv.sources]
 | 
				
			||||||
| 
						 | 
					@ -6417,3 +6417,43 @@ fn install_build_isolation_package() -> Result<()> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Install a package with an unsupported extension.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn invalid_extension() {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.8");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.pip_install()
 | 
				
			||||||
 | 
					        .arg("ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.baz")
 | 
				
			||||||
 | 
					        , @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    error: Failed to parse: `ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.baz`
 | 
				
			||||||
 | 
					      Caused by: Expected direct URL (`https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.baz`) to end in a supported file extension: `.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`
 | 
				
			||||||
 | 
					    ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6.tar.baz
 | 
				
			||||||
 | 
					           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Install a package without unsupported extension.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn no_extension() {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.8");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.pip_install()
 | 
				
			||||||
 | 
					        .arg("ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6")
 | 
				
			||||||
 | 
					        , @r###"
 | 
				
			||||||
 | 
					    success: false
 | 
				
			||||||
 | 
					    exit_code: 2
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    error: Failed to parse: `ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6`
 | 
				
			||||||
 | 
					      Caused by: Expected direct URL (`https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6`) to end in a supported file extension: `.whl`, `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or `.tar.zst`
 | 
				
			||||||
 | 
					    ruff @ https://files.pythonhosted.org/packages/f7/69/96766da2cdb5605e6a31ef2734aff0be17901cefb385b885c2ab88896d76/ruff-0.5.6
 | 
				
			||||||
 | 
					           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue