diff --git a/Cargo.lock b/Cargo.lock index 1362dac8b..4837522cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1104,7 +1104,6 @@ dependencies = [ "cache-key", "distribution-filename", "fs-err", - "git2", "indexmap", "itertools 0.13.0", "once_cell", @@ -2860,7 +2859,9 @@ dependencies = [ name = "pypi-types" version = "0.0.1" dependencies = [ + "anyhow", "chrono", + "git2", "indexmap", "mailparse", "once_cell", @@ -2873,6 +2874,7 @@ dependencies = [ "toml", "tracing", "url", + "uv-git", "uv-normalize", ] @@ -3076,12 +3078,12 @@ dependencies = [ "insta", "itertools 0.13.0", "pep508_rs", + "pypi-types", "regex", "reqwest", "reqwest-middleware", "tempfile", "test-case", - "thiserror", "tokio", "tracing", "unscanny", @@ -4505,6 +4507,7 @@ dependencies = [ "pep508_rs", "platform-tags", "predicates", + "pypi-types", "rayon", "regex", "requirements-txt", @@ -4579,6 +4582,7 @@ dependencies = [ "once_cell", "pep440_rs", "pep508_rs", + "pypi-types", "regex", "rustc-hash", "serde", @@ -4704,6 +4708,7 @@ dependencies = [ "pep508_rs", "poloto", "pretty_assertions", + "pypi-types", "resvg", "rustc-hash", "schemars", diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index 9d907e152..553e3cd12 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use bench::criterion::black_box; use bench::criterion::{criterion_group, criterion_main, measurement::WallTime, Criterion}; use distribution_types::Requirement; @@ -15,9 +17,9 @@ fn resolve_warm_jupyter(c: &mut Criterion) { let cache = &Cache::from_path("../../.cache").unwrap().init().unwrap(); let venv = PythonEnvironment::from_virtualenv(cache).unwrap(); let client = &RegistryClientBuilder::new(cache.clone()).build(); - let manifest = &Manifest::simple(vec![ - Requirement::from_pep508("jupyter".parse().unwrap()).unwrap() - ]); + let manifest = &Manifest::simple(vec![Requirement::from( + pep508_rs::Requirement::from_str("jupyter").unwrap(), + )]); let run = || { runtime @@ -45,13 +47,10 @@ fn resolve_warm_airflow(c: &mut Criterion) { let venv = PythonEnvironment::from_virtualenv(cache).unwrap(); let client = &RegistryClientBuilder::new(cache.clone()).build(); let manifest = &Manifest::simple(vec![ - Requirement::from_pep508("apache-airflow[all]".parse().unwrap()).unwrap(), - Requirement::from_pep508( - "apache-airflow-providers-apache-beam>3.0.0" - .parse() - .unwrap(), - ) - .unwrap(), + Requirement::from(pep508_rs::Requirement::from_str("apache-airflow[all]").unwrap()), + Requirement::from( + pep508_rs::Requirement::from_str("apache-airflow-providers-apache-beam>3.0.0").unwrap(), + ), ]); let run = || { @@ -73,10 +72,10 @@ criterion_main!(uv); mod resolver { use anyhow::Result; - use install_wheel_rs::linker::LinkMode; use once_cell::sync::Lazy; use distribution_types::IndexLocations; + use install_wheel_rs::linker::LinkMode; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; use platform_tags::{Arch, Os, Platform, Tags}; use uv_cache::Cache; diff --git a/crates/distribution-types/Cargo.toml b/crates/distribution-types/Cargo.toml index 9e0bde4d9..1c30d1d55 100644 --- a/crates/distribution-types/Cargo.toml +++ b/crates/distribution-types/Cargo.toml @@ -25,7 +25,6 @@ uv-normalize = { workspace = true } anyhow = { workspace = true } fs-err = { workspace = true } -git2 = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } once_cell = { workspace = true } diff --git a/crates/distribution-types/src/buildable.rs b/crates/distribution-types/src/buildable.rs index 26d7e0aea..e3bee3cdf 100644 --- a/crates/distribution-types/src/buildable.rs +++ b/crates/distribution-types/src/buildable.rs @@ -105,9 +105,9 @@ impl std::fmt::Display for DirectSourceUrl<'_> { pub struct GitSourceUrl<'a> { /// The URL with the revision and subdirectory fragment. pub url: &'a VerbatimUrl, + pub git: &'a GitUrl, /// The URL without the revision and subdirectory fragment. - pub git: Cow<'a, GitUrl>, - pub subdirectory: Option>, + pub subdirectory: Option<&'a Path>, } impl std::fmt::Display for GitSourceUrl<'_> { @@ -120,8 +120,8 @@ impl<'a> From<&'a GitSourceDist> for GitSourceUrl<'a> { fn from(dist: &'a GitSourceDist) -> Self { Self { url: &dist.url, - git: Cow::Borrowed(&dist.git), - subdirectory: dist.subdirectory.as_deref().map(Cow::Borrowed), + git: &dist.git, + subdirectory: dist.subdirectory.as_deref(), } } } diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index a69303dd0..3ad68e8fc 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -4,12 +4,12 @@ use anyhow::{anyhow, Result}; use distribution_filename::WheelFilename; use pep508_rs::VerbatimUrl; -use pypi_types::HashDigest; +use pypi_types::{HashDigest, ParsedPathUrl}; use uv_normalize::PackageName; use crate::{ BuiltDist, Dist, DistributionMetadata, Hashed, InstalledMetadata, InstalledVersion, Name, - ParsedPathUrl, ParsedUrl, SourceDist, VersionOrUrlRef, + ParsedUrl, SourceDist, VersionOrUrlRef, }; /// A built distribution (wheel) that exists in the local cache. diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index cc9025ac5..bcb8fec58 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -42,6 +42,7 @@ use url::Url; use distribution_filename::WheelFilename; use pep440_rs::Version; use pep508_rs::{Pep508Url, VerbatimUrl}; +use pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_git::GitUrl; use uv_normalize::PackageName; @@ -57,7 +58,6 @@ pub use crate::hash::*; pub use crate::id::*; pub use crate::index_url::*; pub use crate::installed::*; -pub use crate::parsed_url::*; pub use crate::prioritized_distribution::*; pub use crate::requirement::*; pub use crate::resolution::*; @@ -77,7 +77,6 @@ mod hash; mod id; mod index_url; mod installed; -mod parsed_url; mod prioritized_distribution; mod requirement; mod resolution; diff --git a/crates/distribution-types/src/requirement.rs b/crates/distribution-types/src/requirement.rs index c36c65fe4..7fe68bee0 100644 --- a/crates/distribution-types/src/requirement.rs +++ b/crates/distribution-types/src/requirement.rs @@ -9,7 +9,7 @@ use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, V use uv_git::{GitReference, GitSha}; use uv_normalize::{ExtraName, PackageName}; -use crate::{ParsedUrl, ParsedUrlError}; +use crate::{ParsedUrl, VerbatimParsedUrl}; /// The requirements of a distribution, an extension over PEP 508's requirements. #[derive(Debug, Clone, Eq, PartialEq)] @@ -44,9 +44,11 @@ impl Requirement { true } } +} +impl From> for Requirement { /// Convert a [`pep508_rs::Requirement`] to a [`Requirement`]. - pub fn from_pep508(requirement: pep508_rs::Requirement) -> Result> { + fn from(requirement: pep508_rs::Requirement) -> Self { let source = match requirement.version_or_url { None => RequirementSource::Registry { specifier: VersionSpecifiers::empty(), @@ -58,17 +60,16 @@ impl Requirement { index: None, }, Some(VersionOrUrl::Url(url)) => { - let direct_url = ParsedUrl::try_from(url.to_url())?; - RequirementSource::from_parsed_url(direct_url, url) + RequirementSource::from_parsed_url(url.parsed_url, url.verbatim) } }; - Ok(Requirement { + Requirement { name: requirement.name, extras: requirement.extras, marker: requirement.marker, source, origin: requirement.origin, - }) + } } } diff --git a/crates/distribution-types/src/specified_requirement.rs b/crates/distribution-types/src/specified_requirement.rs index 5f401ef2d..4287cfd3c 100644 --- a/crates/distribution-types/src/specified_requirement.rs +++ b/crates/distribution-types/src/specified_requirement.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter}; use pep508_rs::{MarkerEnvironment, UnnamedRequirement}; use uv_normalize::ExtraName; -use crate::{ParsedUrl, ParsedUrlError, Requirement, RequirementSource}; +use crate::{Requirement, RequirementSource, VerbatimParsedUrl}; /// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only /// hashes but in the future also editable and similar information. @@ -29,7 +29,7 @@ pub enum UnresolvedRequirement { /// `tool.uv.sources`. Named(Requirement), /// A PEP 508-like, direct URL dependency specifier. - Unnamed(UnnamedRequirement), + Unnamed(UnnamedRequirement), } impl Display for UnresolvedRequirement { @@ -64,17 +64,13 @@ impl UnresolvedRequirement { } /// Return the version specifier or URL for the requirement. - pub fn source(&self) -> Result, Box> { - // TODO(konsti): This is a bad place to raise errors, we should have parsed the url earlier. + pub fn source(&self) -> Cow<'_, RequirementSource> { match self { - Self::Named(requirement) => Ok(Cow::Borrowed(&requirement.source)), - Self::Unnamed(requirement) => { - let parsed_url = ParsedUrl::try_from(requirement.url.to_url())?; - Ok(Cow::Owned(RequirementSource::from_parsed_url( - parsed_url, - requirement.url.clone(), - ))) - } + Self::Named(requirement) => Cow::Borrowed(&requirement.source), + Self::Unnamed(requirement) => Cow::Owned(RequirementSource::from_parsed_url( + requirement.url.parsed_url.clone(), + requirement.url.verbatim.clone(), + )), } } } diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 99065fc17..558dbf7b9 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -16,7 +16,6 @@ #![warn(missing_docs)] -use cursor::Cursor; #[cfg(feature = "pyo3")] use std::collections::hash_map::DefaultHasher; use std::collections::HashSet; @@ -39,18 +38,18 @@ use thiserror::Error; use unicode_width::UnicodeWidthChar; use url::Url; +use cursor::Cursor; pub use marker::{ ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, MarkerTree, MarkerValue, MarkerValueString, MarkerValueVersion, MarkerWarningKind, StringVersion, }; +pub use origin::RequirementOrigin; #[cfg(feature = "pyo3")] use pep440_rs::PyVersion; use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; #[cfg(feature = "non-pep508-extensions")] -pub use unnamed::UnnamedRequirement; -// Parity with the crates.io version of pep508_rs -pub use origin::RequirementOrigin; +pub use unnamed::{UnnamedRequirement, UnnamedRequirementUrl}; pub use uv_normalize::{ExtraName, InvalidNameError, PackageName}; pub use verbatim_url::{ expand_env_vars, split_scheme, strip_host, Scheme, VerbatimUrl, VerbatimUrlError, @@ -123,7 +122,7 @@ impl Display for Pep508Error { } } -/// We need this to allow e.g. anyhow's `.context()` +/// We need this to allow anyhow's `.context()` and `AsDynError`. impl> std::error::Error for Pep508Error {} #[cfg(feature = "pyo3")] @@ -155,17 +154,6 @@ pub struct Requirement { pub origin: Option, } -impl Requirement { - /// Set the source file containing the requirement. - #[must_use] - pub fn with_origin(self, origin: RequirementOrigin) -> Self { - Self { - origin: Some(origin), - ..self - } - } -} - impl Display for Requirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name)?; @@ -453,10 +441,19 @@ impl Requirement { ..self } } + + /// Set the source file containing the requirement. + #[must_use] + pub fn with_origin(self, origin: RequirementOrigin) -> Self { + Self { + origin: Some(origin), + ..self + } + } } /// Type to parse URLs from `name @ ` into. Defaults to [`url::Url`]. -pub trait Pep508Url: Clone + Display + Debug { +pub trait Pep508Url: Display + Debug + Sized { /// String to URL parsing error type Err: Error + Debug; @@ -1136,7 +1133,7 @@ mod tests { #[cfg(feature = "non-pep508-extensions")] fn parse_unnamed_err(input: &str) -> String { - crate::UnnamedRequirement::from_str(input) + crate::UnnamedRequirement::::from_str(input) .unwrap_err() .to_string() } @@ -1256,7 +1253,7 @@ mod tests { #[test] #[cfg(feature = "non-pep508-extensions")] fn direct_url_no_extras() { - let numpy = crate::UnnamedRequirement::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap(); + let numpy = crate::UnnamedRequirement::::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap(); assert_eq!(numpy.url.to_string(), "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl"); assert_eq!(numpy.extras, vec![]); } @@ -1264,9 +1261,10 @@ mod tests { #[test] #[cfg(all(unix, feature = "non-pep508-extensions"))] fn direct_url_extras() { - let numpy = - crate::UnnamedRequirement::from_str("/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]") - .unwrap(); + let numpy = crate::UnnamedRequirement::::from_str( + "/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]", + ) + .unwrap(); assert_eq!( numpy.url.to_string(), "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl" @@ -1277,7 +1275,7 @@ mod tests { #[test] #[cfg(all(windows, feature = "non-pep508-extensions"))] fn direct_url_extras() { - let numpy = crate::UnnamedRequirement::from_str( + let numpy = crate::UnnamedRequirement::::from_str( "C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]", ) .unwrap(); @@ -1459,7 +1457,8 @@ mod tests { fn test_marker_parsing() { let marker = r#"python_version == "2.7" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))"#; let actual = - parse_markers_cursor::(&mut Cursor::new(marker), &mut TracingReporter).unwrap(); + parse_markers_cursor::(&mut Cursor::new(marker), &mut TracingReporter) + .unwrap(); let expected = MarkerTree::And(vec![ MarkerTree::Expression(MarkerExpression::Version { key: MarkerValueVersion::PythonVersion, diff --git a/crates/pep508-rs/src/marker.rs b/crates/pep508-rs/src/marker.rs index 99716f3ae..b3d9ba954 100644 --- a/crates/pep508-rs/src/marker.rs +++ b/crates/pep508-rs/src/marker.rs @@ -1550,6 +1550,11 @@ impl FromStr for MarkerTree { } impl MarkerTree { + /// Like [`FromStr::from_str`], but the caller chooses the return type generic. + pub fn parse_str(markers: &str) -> Result> { + parse_markers(markers, &mut TracingReporter) + } + /// Parse a [`MarkerTree`] from a string with the given reporter. pub fn parse_reporter( markers: &str, diff --git a/crates/pep508-rs/src/unnamed.rs b/crates/pep508-rs/src/unnamed.rs index 778b0237f..dced751bf 100644 --- a/crates/pep508-rs/src/unnamed.rs +++ b/crates/pep508-rs/src/unnamed.rs @@ -1,28 +1,74 @@ -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use std::hash::Hash; use std::path::Path; use std::str::FromStr; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; - use uv_fs::normalize_url_path; use uv_normalize::ExtraName; use crate::marker::parse_markers_cursor; use crate::{ expand_env_vars, parse_extras_cursor, split_extras, split_scheme, strip_host, Cursor, - MarkerEnvironment, MarkerTree, Pep508Error, Pep508ErrorSource, Reporter, RequirementOrigin, - Scheme, TracingReporter, VerbatimUrl, VerbatimUrlError, + MarkerEnvironment, MarkerTree, Pep508Error, Pep508ErrorSource, Pep508Url, Reporter, + RequirementOrigin, Scheme, TracingReporter, VerbatimUrl, VerbatimUrlError, }; +/// An extension over [`Pep508Url`] that also supports parsing unnamed requirements, namely paths. +/// +/// The error type is fixed to the same as the [`Pep508Url`] impl error. +pub trait UnnamedRequirementUrl: Pep508Url { + /// Parse a URL from a relative or absolute path. + fn parse_path(path: impl AsRef, working_dir: impl AsRef) + -> Result; + + /// Parse a URL from an absolute path. + fn parse_absolute_path(path: impl AsRef) -> Result; + + /// Parse a URL from a string. + fn parse_unnamed_url(given: impl AsRef) -> Result; + + /// Set the verbatim representation of the URL. + #[must_use] + fn with_given(self, given: impl Into) -> Self; + + /// Return the original string as given by the user, if available. + fn given(&self) -> Option<&str>; +} + +impl UnnamedRequirementUrl for VerbatimUrl { + fn parse_path( + path: impl AsRef, + working_dir: impl AsRef, + ) -> Result { + Self::parse_path(path, working_dir) + } + + fn parse_absolute_path(path: impl AsRef) -> Result { + Self::parse_absolute_path(path) + } + + fn parse_unnamed_url(given: impl AsRef) -> Result { + Ok(Self::parse_url(given)?) + } + + fn with_given(self, given: impl Into) -> Self { + self.with_given(given) + } + + fn given(&self) -> Option<&str> { + self.given() + } +} + /// A PEP 508-like, direct URL dependency specifier without a package name. /// /// In a `requirements.txt` file, the name of the package is optional for direct URL /// dependencies. This isn't compliant with PEP 508, but is common in `requirements.txt`, which /// is implementation-defined. #[derive(Hash, Debug, Clone, Eq, PartialEq)] -pub struct UnnamedRequirement { +pub struct UnnamedRequirement { /// The direct URL that defines the version specifier. - pub url: VerbatimUrl, + pub url: Url, /// The list of extras such as `security`, `tests` in /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. pub extras: Vec, @@ -34,7 +80,7 @@ pub struct UnnamedRequirement { pub origin: Option, } -impl UnnamedRequirement { +impl UnnamedRequirement { /// Returns whether the markers apply for the given environment pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { self.evaluate_optional_environment(Some(env), extras) @@ -61,9 +107,22 @@ impl UnnamedRequirement { ..self } } + + /// Parse a PEP 508-like direct URL requirement without a package name. + pub fn parse( + input: &str, + working_dir: impl AsRef, + reporter: &mut impl Reporter, + ) -> Result> { + parse_unnamed_requirement( + &mut Cursor::new(input), + Some(working_dir.as_ref()), + reporter, + ) + } } -impl Display for UnnamedRequirement { +impl Display for UnnamedRequirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.url)?; if !self.extras.is_empty() { @@ -84,29 +143,8 @@ impl Display for UnnamedRequirement { } } -/// -impl<'de> Deserialize<'de> for UnnamedRequirement { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(de::Error::custom) - } -} - -/// -impl Serialize for UnnamedRequirement { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl FromStr for UnnamedRequirement { - type Err = Pep508Error; +impl FromStr for UnnamedRequirement { + type Err = Pep508Error; /// Parse a PEP 508-like direct URL requirement without a package name. fn from_str(input: &str) -> Result { @@ -114,33 +152,18 @@ impl FromStr for UnnamedRequirement { } } -impl UnnamedRequirement { - /// Parse a PEP 508-like direct URL requirement without a package name. - pub fn parse( - input: &str, - working_dir: impl AsRef, - reporter: &mut impl Reporter, - ) -> Result> { - parse_unnamed_requirement( - &mut Cursor::new(input), - Some(working_dir.as_ref()), - reporter, - ) - } -} - /// Parse a PEP 508-like direct URL specifier without a package name. /// /// Unlike pip, we allow extras on URLs and paths. -fn parse_unnamed_requirement( +fn parse_unnamed_requirement( cursor: &mut Cursor, working_dir: Option<&Path>, reporter: &mut impl Reporter, -) -> Result> { +) -> Result, Pep508Error> { cursor.eat_whitespace(); // Parse the URL itself, along with any extras. - let (url, extras) = parse_unnamed_url(cursor, working_dir)?; + let (url, extras) = parse_unnamed_url::(cursor, working_dir)?; let requirement_end = cursor.pos(); // wsp* @@ -191,13 +214,13 @@ fn parse_unnamed_requirement( /// Create a `VerbatimUrl` to represent the requirement, and extracts any extras at the end of the /// URL, to comply with the non-PEP 508 extensions. -fn preprocess_unnamed_url( +fn preprocess_unnamed_url( url: &str, #[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused))] working_dir: Option<&Path>, cursor: &Cursor, start: usize, len: usize, -) -> Result<(VerbatimUrl, Vec), Pep508Error> { +) -> Result<(Url, Vec), Pep508Error> { // Split extras _before_ expanding the URL. We assume that the extras are not environment // variables. If we parsed the extras after expanding the URL, then the verbatim representation // of the URL itself would be ambiguous, since it would consist of the environment variable, @@ -235,9 +258,9 @@ fn preprocess_unnamed_url( #[cfg(feature = "non-pep508-extensions")] if let Some(working_dir) = working_dir { - let url = VerbatimUrl::parse_path(path.as_ref(), working_dir) + let url = Url::parse_path(path.as_ref(), working_dir) .map_err(|err| Pep508Error { - message: Pep508ErrorSource::::UrlError(err), + message: Pep508ErrorSource::UrlError(err), start, len, input: cursor.to_string(), @@ -246,9 +269,9 @@ fn preprocess_unnamed_url( return Ok((url, extras)); } - let url = VerbatimUrl::parse_absolute_path(path.as_ref()) + let url = Url::parse_absolute_path(path.as_ref()) .map_err(|err| Pep508Error { - message: Pep508ErrorSource::::UrlError(err), + message: Pep508ErrorSource::UrlError(err), start, len, input: cursor.to_string(), @@ -259,11 +282,9 @@ fn preprocess_unnamed_url( // Ex) `https://download.pytorch.org/whl/torch_stable.html` Some(_) => { // Ex) `https://download.pytorch.org/whl/torch_stable.html` - let url = VerbatimUrl::parse_url(expanded.as_ref()) + let url = Url::parse_unnamed_url(expanded.as_ref()) .map_err(|err| Pep508Error { - message: Pep508ErrorSource::::UrlError(VerbatimUrlError::Url( - err, - )), + message: Pep508ErrorSource::UrlError(err), start, len, input: cursor.to_string(), @@ -275,9 +296,9 @@ fn preprocess_unnamed_url( // Ex) `C:\Users\ferris\wheel-0.42.0.tar.gz` _ => { if let Some(working_dir) = working_dir { - let url = VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + let url = Url::parse_path(expanded.as_ref(), working_dir) .map_err(|err| Pep508Error { - message: Pep508ErrorSource::::UrlError(err), + message: Pep508ErrorSource::UrlError(err), start, len, input: cursor.to_string(), @@ -286,7 +307,7 @@ fn preprocess_unnamed_url( return Ok((url, extras)); } - let url = VerbatimUrl::parse_absolute_path(expanded.as_ref()) + let url = Url::parse_absolute_path(expanded.as_ref()) .map_err(|err| Pep508Error { message: Pep508ErrorSource::UrlError(err), start, @@ -300,9 +321,9 @@ fn preprocess_unnamed_url( } else { // Ex) `../editable/` if let Some(working_dir) = working_dir { - let url = VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + let url = Url::parse_path(expanded.as_ref(), working_dir) .map_err(|err| Pep508Error { - message: Pep508ErrorSource::::UrlError(err), + message: Pep508ErrorSource::UrlError(err), start, len, input: cursor.to_string(), @@ -311,7 +332,7 @@ fn preprocess_unnamed_url( return Ok((url, extras)); } - let url = VerbatimUrl::parse_absolute_path(expanded.as_ref()) + let url = Url::parse_absolute_path(expanded.as_ref()) .map_err(|err| Pep508Error { message: Pep508ErrorSource::UrlError(err), start, @@ -329,10 +350,10 @@ fn preprocess_unnamed_url( /// For example: /// - `https://download.pytorch.org/whl/torch_stable.html[dev]` /// - `../editable[dev]` -fn parse_unnamed_url( +fn parse_unnamed_url( cursor: &mut Cursor, working_dir: Option<&Path>, -) -> Result<(VerbatimUrl, Vec), Pep508Error> { +) -> Result<(Url, Vec), Pep508Error> { // wsp* cursor.eat_whitespace(); // diff --git a/crates/pypi-types/Cargo.toml b/crates/pypi-types/Cargo.toml index 94896ff1c..c9aa42e0c 100644 --- a/crates/pypi-types/Cargo.toml +++ b/crates/pypi-types/Cargo.toml @@ -16,8 +16,11 @@ workspace = true pep440_rs = { workspace = true } pep508_rs = { workspace = true } uv-normalize = { workspace = true } +uv-git = { workspace = true } +anyhow = { workspace = true } chrono = { workspace = true, features = ["serde"] } +git2 = { workspace = true } indexmap = { workspace = true, features = ["serde"] } mailparse = { workspace = true } once_cell = { workspace = true } diff --git a/crates/pypi-types/src/lenient_requirement.rs b/crates/pypi-types/src/lenient_requirement.rs index 461730c47..1e0582558 100644 --- a/crates/pypi-types/src/lenient_requirement.rs +++ b/crates/pypi-types/src/lenient_requirement.rs @@ -7,7 +7,9 @@ use serde::{de, Deserialize, Deserializer, Serialize}; use tracing::warn; use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError}; -use pep508_rs::{Pep508Error, Pep508Url, Requirement, VerbatimUrl}; +use pep508_rs::{Pep508Error, Pep508Url, Requirement}; + +use crate::VerbatimParsedUrl; /// Ex) `>=7.2.0<8.0.0` static MISSING_COMMA: Lazy = Lazy::new(|| Regex::new(r"(\d)([<>=~^!])").unwrap()); @@ -114,7 +116,7 @@ fn parse_with_fixups>(input: &str, type_name: &str) - /// Like [`Requirement`], but attempts to correct some common errors in user-provided requirements. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct LenientRequirement(Requirement); +pub struct LenientRequirement(Requirement); impl FromStr for LenientRequirement { type Err = Pep508Error; diff --git a/crates/pypi-types/src/lib.rs b/crates/pypi-types/src/lib.rs index f1efe6776..086437ba5 100644 --- a/crates/pypi-types/src/lib.rs +++ b/crates/pypi-types/src/lib.rs @@ -2,6 +2,7 @@ pub use base_url::*; pub use direct_url::*; pub use lenient_requirement::*; pub use metadata::*; +pub use parsed_url::*; pub use scheme::*; pub use simple_json::*; @@ -9,5 +10,6 @@ mod base_url; mod direct_url; mod lenient_requirement; mod metadata; +mod parsed_url; mod scheme; mod simple_json; diff --git a/crates/pypi-types/src/metadata.rs b/crates/pypi-types/src/metadata.rs index a877bb3aa..c1b11b33b 100644 --- a/crates/pypi-types/src/metadata.rs +++ b/crates/pypi-types/src/metadata.rs @@ -9,11 +9,11 @@ use thiserror::Error; use tracing::warn; use pep440_rs::{Version, VersionParseError, VersionSpecifiers, VersionSpecifiersParseError}; -use pep508_rs::{Pep508Error, Requirement, VerbatimUrl}; +use pep508_rs::{Pep508Error, Requirement}; use uv_normalize::{ExtraName, InvalidNameError, PackageName}; use crate::lenient_requirement::LenientRequirement; -use crate::LenientVersionSpecifiers; +use crate::{LenientVersionSpecifiers, VerbatimParsedUrl}; /// Python Package Metadata 2.3 as specified in /// . @@ -29,7 +29,7 @@ pub struct Metadata23 { pub name: PackageName, pub version: Version, // Optional fields - pub requires_dist: Vec>, + pub requires_dist: Vec>, pub requires_python: Option, pub provides_extras: Vec, } @@ -50,7 +50,7 @@ pub enum MetadataError { #[error(transparent)] Pep440Error(#[from] VersionSpecifiersParseError), #[error(transparent)] - Pep508Error(#[from] Pep508Error), + Pep508Error(#[from] Box>), #[error(transparent)] InvalidName(#[from] InvalidNameError), #[error("Invalid `Metadata-Version` field: {0}")] @@ -61,6 +61,12 @@ pub enum MetadataError { DynamicField(&'static str), } +impl From> for MetadataError { + fn from(error: Pep508Error) -> Self { + Self::Pep508Error(Box::new(error)) + } +} + /// From impl Metadata23 { /// Parse the [`Metadata23`] from a `METADATA` file, as included in a built distribution (wheel). diff --git a/crates/distribution-types/src/parsed_url.rs b/crates/pypi-types/src/parsed_url.rs similarity index 72% rename from crates/distribution-types/src/parsed_url.rs rename to crates/pypi-types/src/parsed_url.rs index a34401099..04485143b 100644 --- a/crates/distribution-types/src/parsed_url.rs +++ b/crates/pypi-types/src/parsed_url.rs @@ -1,12 +1,14 @@ -use std::path::PathBuf; +use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; -use anyhow::{Error, Result}; use thiserror::Error; -use url::Url; +use url::{ParseError, Url}; -use pep508_rs::VerbatimUrl; +use pep508_rs::{Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError}; use uv_git::{GitSha, GitUrl}; +use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind}; + #[derive(Debug, Error)] pub enum ParsedUrlError { #[error("Unsupported URL prefix `{prefix}` in URL: `{url}` ({message})")] @@ -20,7 +22,9 @@ pub enum ParsedUrlError { #[error("Failed to parse Git reference from URL: `{0}`")] GitShaParse(Url, #[source] git2::Error), #[error("Not a valid URL: `{0}`")] - UrlParse(String, #[source] url::ParseError), + UrlParse(String, #[source] ParseError), + #[error(transparent)] + VerbatimUrl(#[from] VerbatimUrlError), } #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)] @@ -29,6 +33,105 @@ pub struct VerbatimParsedUrl { pub verbatim: VerbatimUrl, } +impl Pep508Url for VerbatimParsedUrl { + type Err = ParsedUrlError; + + fn parse_url(url: &str, working_dir: Option<&Path>) -> Result { + let verbatim_url = ::parse_url(url, working_dir)?; + Ok(Self { + parsed_url: ParsedUrl::try_from(verbatim_url.to_url())?, + verbatim: verbatim_url, + }) + } +} + +impl UnnamedRequirementUrl for VerbatimParsedUrl { + fn parse_path( + path: impl AsRef, + working_dir: impl AsRef, + ) -> Result { + let verbatim = VerbatimUrl::parse_path(&path, &working_dir)?; + let parsed_path_url = ParsedPathUrl { + url: verbatim.to_url(), + path: working_dir.as_ref().join(path), + editable: false, + }; + Ok(Self { + parsed_url: ParsedUrl::Path(parsed_path_url), + verbatim, + }) + } + + fn parse_absolute_path(path: impl AsRef) -> Result { + let verbatim = VerbatimUrl::parse_absolute_path(&path)?; + let parsed_path_url = ParsedPathUrl { + url: verbatim.to_url(), + path: path.as_ref().to_path_buf(), + editable: false, + }; + Ok(Self { + parsed_url: ParsedUrl::Path(parsed_path_url), + verbatim, + }) + } + + fn parse_unnamed_url(url: impl AsRef) -> Result { + let verbatim = ::parse_unnamed_url(&url)?; + Ok(Self { + parsed_url: ParsedUrl::try_from(verbatim.to_url())?, + verbatim, + }) + } + + fn with_given(self, given: impl Into) -> Self { + Self { + verbatim: self.verbatim.with_given(given), + ..self + } + } + + fn given(&self) -> Option<&str> { + self.verbatim.given() + } +} + +impl Display for VerbatimParsedUrl { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.verbatim, f) + } +} + +impl TryFrom for VerbatimParsedUrl { + type Error = ParsedUrlError; + + fn try_from(verbatim_url: VerbatimUrl) -> Result { + let parsed_url = ParsedUrl::try_from(verbatim_url.to_url())?; + Ok(Self { + parsed_url, + verbatim: verbatim_url, + }) + } +} + +impl serde::ser::Serialize for VerbatimParsedUrl { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + self.verbatim.serialize(serializer) + } +} + +impl<'de> serde::de::Deserialize<'de> for VerbatimParsedUrl { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let verbatim_url = VerbatimUrl::deserialize(deserializer)?; + Self::try_from(verbatim_url).map_err(serde::de::Error::custom) + } +} + /// We support three types of URLs for distributions: /// * The path to a file or directory (`file://`) /// * A Git repository (`git+https://` or `git+ssh://`), optionally with a subdirectory and/or @@ -124,7 +227,7 @@ fn get_subdirectory(url: &Url) -> Option { } /// Return the Git reference of the given URL, if it exists. -pub fn git_reference(url: Url) -> Result, Error> { +pub fn git_reference(url: Url) -> Result, Box> { let ParsedGitUrl { url, .. } = ParsedGitUrl::try_from(url)?; Ok(url.precise()) } @@ -172,10 +275,10 @@ impl TryFrom for ParsedUrl { } } -impl TryFrom<&ParsedUrl> for pypi_types::DirectUrl { - type Error = Error; +impl TryFrom<&ParsedUrl> for DirectUrl { + type Error = ParsedUrlError; - fn try_from(value: &ParsedUrl) -> std::result::Result { + fn try_from(value: &ParsedUrl) -> Result { match value { ParsedUrl::Path(value) => Self::try_from(value), ParsedUrl::Git(value) => Self::try_from(value), @@ -184,26 +287,26 @@ impl TryFrom<&ParsedUrl> for pypi_types::DirectUrl { } } -impl TryFrom<&ParsedPathUrl> for pypi_types::DirectUrl { - type Error = Error; +impl TryFrom<&ParsedPathUrl> for DirectUrl { + type Error = ParsedUrlError; fn try_from(value: &ParsedPathUrl) -> Result { Ok(Self::LocalDirectory { url: value.url.to_string(), - dir_info: pypi_types::DirInfo { + dir_info: DirInfo { editable: value.editable.then_some(true), }, }) } } -impl TryFrom<&ParsedArchiveUrl> for pypi_types::DirectUrl { - type Error = Error; +impl TryFrom<&ParsedArchiveUrl> for DirectUrl { + type Error = ParsedUrlError; fn try_from(value: &ParsedArchiveUrl) -> Result { Ok(Self::ArchiveUrl { url: value.url.to_string(), - archive_info: pypi_types::ArchiveInfo { + archive_info: ArchiveInfo { hash: None, hashes: None, }, @@ -212,14 +315,14 @@ impl TryFrom<&ParsedArchiveUrl> for pypi_types::DirectUrl { } } -impl TryFrom<&ParsedGitUrl> for pypi_types::DirectUrl { - type Error = Error; +impl TryFrom<&ParsedGitUrl> for DirectUrl { + type Error = ParsedUrlError; fn try_from(value: &ParsedGitUrl) -> Result { Ok(Self::VcsUrl { url: value.url.repository().to_string(), - vcs_info: pypi_types::VcsInfo { - vcs: pypi_types::VcsKind::Git, + vcs_info: VcsInfo { + vcs: VcsKind::Git, commit_id: value.url.precise().as_ref().map(ToString::to_string), requested_revision: value.url.reference().as_str().map(ToString::to_string), }, diff --git a/crates/requirements-txt/Cargo.toml b/crates/requirements-txt/Cargo.toml index 7c4a63c87..ccf0c4158 100644 --- a/crates/requirements-txt/Cargo.toml +++ b/crates/requirements-txt/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] distribution-types = { workspace = true } pep508_rs = { workspace = true } +pypi-types = { workspace = true } uv-client = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } @@ -25,7 +26,6 @@ fs-err = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } -thiserror = { workspace = true } tracing = { workspace = true } unscanny = { workspace = true } url = { workspace = true } diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 243c4a0e0..e2ab9aac0 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -44,13 +44,12 @@ use tracing::instrument; use unscanny::{Pattern, Scanner}; use url::Url; -use distribution_types::{ - ParsedUrlError, Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification, -}; +use distribution_types::{Requirement, UnresolvedRequirement, UnresolvedRequirementSpecification}; use pep508_rs::{ expand_env_vars, split_scheme, strip_host, Extras, MarkerTree, Pep508Error, Pep508ErrorSource, RequirementOrigin, Scheme, VerbatimUrl, }; +use pypi_types::VerbatimParsedUrl; #[cfg(feature = "http")] use uv_client::BaseClient; use uv_client::BaseClientBuilder; @@ -59,7 +58,7 @@ use uv_fs::{normalize_url_path, Simplified}; use uv_normalize::ExtraName; use uv_warnings::warn_user; -pub use crate::requirement::{RequirementsTxtRequirement, RequirementsTxtRequirementError}; +pub use crate::requirement::RequirementsTxtRequirement; mod requirement; @@ -203,7 +202,7 @@ impl EditableRequirement { ) -> Result { // Identify the markers. let (given, marker) = if let Some((requirement, marker)) = Self::split_markers(given) { - let marker = MarkerTree::from_str(marker).map_err(|err| { + let marker = MarkerTree::parse_str(marker).map_err(|err| { // Map from error on the markers to error on the whole requirement. let err = Pep508Error { message: err.message, @@ -216,14 +215,14 @@ impl EditableRequirement { RequirementsTxtParserError::Pep508 { start: err.start, end: err.start + err.len, - source: err, + source: Box::new(err), } } Pep508ErrorSource::UnsupportedRequirement(_) => { RequirementsTxtParserError::UnsupportedRequirement { start: err.start, end: err.start + err.len, - source: err, + source: Box::new(err), } } } @@ -248,14 +247,14 @@ impl EditableRequirement { RequirementsTxtParserError::Pep508 { start: err.start, end: err.start + err.len, - source: err, + source: Box::new(err), } } Pep508ErrorSource::UnsupportedRequirement(_) => { RequirementsTxtParserError::UnsupportedRequirement { start: err.start, end: err.start + err.len, - source: err, + source: Box::new(err), } } } @@ -403,21 +402,19 @@ pub struct RequirementEntry { // We place the impl here instead of next to `UnresolvedRequirementSpecification` because // `UnresolvedRequirementSpecification` is defined in `distribution-types` and `requirements-txt` // depends on `distribution-types`. -impl TryFrom for UnresolvedRequirementSpecification { - type Error = Box; - - fn try_from(value: RequirementEntry) -> Result { - Ok(Self { +impl From for UnresolvedRequirementSpecification { + fn from(value: RequirementEntry) -> Self { + Self { requirement: match value.requirement { RequirementsTxtRequirement::Named(named) => { - UnresolvedRequirement::Named(Requirement::from_pep508(named)?) + UnresolvedRequirement::Named(Requirement::from(named)) } RequirementsTxtRequirement::Unnamed(unnamed) => { UnresolvedRequirement::Unnamed(unnamed) } }, hashes: value.hashes, - }) + } } } @@ -427,7 +424,7 @@ pub struct RequirementsTxt { /// The actual requirements with the hashes. pub requirements: Vec, /// Constraints included with `-c`. - pub constraints: Vec, + pub constraints: Vec>, /// Editables with `-e`. pub editables: Vec, /// The index URL, specified with `--index-url`. @@ -914,30 +911,10 @@ fn parse_requirement_and_hashes( requirement } }) - .map_err(|err| match err { - RequirementsTxtRequirementError::ParsedUrl(err) => { - RequirementsTxtParserError::ParsedUrl { - source: err, - start, - end, - } - } - RequirementsTxtRequirementError::Pep508(err) => match err.message { - Pep508ErrorSource::String(_) | Pep508ErrorSource::UrlError(_) => { - RequirementsTxtParserError::Pep508 { - source: err, - start, - end, - } - } - Pep508ErrorSource::UnsupportedRequirement(_) => { - RequirementsTxtParserError::UnsupportedRequirement { - source: err, - start, - end, - } - } - }, + .map_err(|err| RequirementsTxtParserError::Pep508 { + source: err, + start, + end, })?; let hashes = if has_hashes { @@ -1068,17 +1045,17 @@ pub enum RequirementsTxtParserError { column: usize, }, UnsupportedRequirement { - source: Pep508Error, + source: Box>, start: usize, end: usize, }, Pep508 { - source: Pep508Error, + source: Box>, start: usize, end: usize, }, ParsedUrl { - source: Box, + source: Box>, start: usize, end: usize, }, diff --git a/crates/requirements-txt/src/requirement.rs b/crates/requirements-txt/src/requirement.rs index 094e9e126..91b280cca 100644 --- a/crates/requirements-txt/src/requirement.rs +++ b/crates/requirements-txt/src/requirement.rs @@ -1,11 +1,9 @@ use std::path::Path; -use thiserror::Error; - -use distribution_types::ParsedUrlError; use pep508_rs::{ Pep508Error, Pep508ErrorSource, RequirementOrigin, TracingReporter, UnnamedRequirement, }; +use pypi_types::VerbatimParsedUrl; /// A requirement specifier in a `requirements.txt` file. /// @@ -15,9 +13,9 @@ use pep508_rs::{ pub enum RequirementsTxtRequirement { /// The uv-specific superset over PEP 508 requirements specifier incorporating /// `tool.uv.sources`. - Named(pep508_rs::Requirement), + Named(pep508_rs::Requirement), /// A PEP 508-like, direct URL dependency specifier. - Unnamed(UnnamedRequirement), + Unnamed(UnnamedRequirement), } impl RequirementsTxtRequirement { @@ -31,20 +29,12 @@ impl RequirementsTxtRequirement { } } -#[derive(Debug, Error)] -pub enum RequirementsTxtRequirementError { - #[error(transparent)] - ParsedUrl(#[from] Box), - #[error(transparent)] - Pep508(#[from] Pep508Error), -} - impl RequirementsTxtRequirement { /// Parse a requirement as seen in a `requirements.txt` file. pub fn parse( input: &str, working_dir: impl AsRef, - ) -> Result { + ) -> Result>> { // Attempt to parse as a PEP 508-compliant requirement. match pep508_rs::Requirement::parse(input, &working_dir) { Ok(requirement) => Ok(Self::Named(requirement)), @@ -57,8 +47,9 @@ impl RequirementsTxtRequirement { &mut TracingReporter, )?)) } - _ => Err(RequirementsTxtRequirementError::Pep508(err)), + _ => Err(err), }, } + .map_err(Box::new) } } diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap index 3b03dc8ed..81071f236 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap @@ -35,25 +35,47 @@ RequirementsTxt { ], version_or_url: Some( Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas", - query: None, - fragment: None, - }, - given: Some( - "https://github.com/pandas-dev/pandas", + VerbatimParsedUrl { + parsed_url: Archive( + ParsedArchiveUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), + ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + subdirectory: None, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), + ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + given: Some( + "https://github.com/pandas-dev/pandas", + ), + }, }, ), ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap index f935f7e45..d81533195 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -7,21 +7,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "./scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/./scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/packages/black_editable", + ), + }, }, extras: [], marker: None, @@ -37,21 +56,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "./scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/./scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/packages/black_editable", + ), + }, }, extras: [ ExtraName( @@ -71,21 +109,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "file:///scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "file:///scripts/packages/black_editable", + ), + }, }, extras: [], marker: None, diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap index 3b03dc8ed..81071f236 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap @@ -35,25 +35,47 @@ RequirementsTxt { ], version_or_url: Some( Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas", - query: None, - fragment: None, - }, - given: Some( - "https://github.com/pandas-dev/pandas", + VerbatimParsedUrl { + parsed_url: Archive( + ParsedArchiveUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), + ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + subdirectory: None, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "github.com", + ), + ), + port: None, + path: "/pandas-dev/pandas", + query: None, + fragment: None, + }, + given: Some( + "https://github.com/pandas-dev/pandas", + ), + }, }, ), ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap index 2611057d7..7c18a8c5f 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap @@ -7,21 +7,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "./scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/./scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/packages/black_editable", + ), + }, }, extras: [], marker: None, @@ -37,21 +56,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "./scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/./scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "./scripts/packages/black_editable", + ), + }, }, extras: [ ExtraName( @@ -71,21 +109,40 @@ RequirementsTxt { RequirementEntry { requirement: Unnamed( UnnamedRequirement { - url: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, - given: Some( - "file:///scripts/packages/black_editable", + url: VerbatimParsedUrl { + parsed_url: Path( + ParsedPathUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + path: "/scripts/packages/black_editable", + editable: false, + }, ), + verbatim: VerbatimUrl { + url: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "//scripts/packages/black_editable", + query: None, + fragment: None, + }, + given: Some( + "file:///scripts/packages/black_editable", + ), + }, }, extras: [], marker: None, diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index dbd58e40a..b80ad792a 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -17,6 +17,7 @@ workspace = true distribution-types = { workspace = true } pep440_rs = { workspace = true } pep508_rs = { workspace = true } +pypi-types = { workspace = true } uv-fs = { workspace = true } uv-interpreter = { workspace = true } uv-types = { workspace = true } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index 4c5dd36ed..c06131f10 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -25,9 +25,10 @@ use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{debug, info_span, instrument, Instrument}; -use distribution_types::{ParsedUrlError, Requirement, Resolution}; +use distribution_types::{Requirement, Resolution}; use pep440_rs::Version; use pep508_rs::PackageName; +use pypi_types::VerbatimParsedUrl; use uv_configuration::{BuildKind, ConfigSettings, SetupPyStrategy}; use uv_fs::{PythonExt, Simplified}; use uv_interpreter::{Interpreter, PythonEnvironment}; @@ -66,18 +67,16 @@ static WHEEL_NOT_FOUND_RE: Lazy = static DEFAULT_BACKEND: Lazy = Lazy::new(|| Pep517Backend { backend: "setuptools.build_meta:__legacy__".to_string(), backend_path: None, - requirements: vec![Requirement::from_pep508( + requirements: vec![Requirement::from( pep508_rs::Requirement::from_str("setuptools >= 40.8.0").unwrap(), - ) - .unwrap()], + )], }); /// The requirements for `--legacy-setup-py` builds. static SETUP_PY_REQUIREMENTS: Lazy<[Requirement; 2]> = Lazy::new(|| { [ - Requirement::from_pep508(pep508_rs::Requirement::from_str("setuptools >= 40.8.0").unwrap()) - .unwrap(), - Requirement::from_pep508(pep508_rs::Requirement::from_str("wheel").unwrap()).unwrap(), + Requirement::from(pep508_rs::Requirement::from_str("setuptools >= 40.8.0").unwrap()), + Requirement::from(pep508_rs::Requirement::from_str("wheel").unwrap()), ] }); @@ -116,8 +115,6 @@ pub enum Error { }, #[error("Failed to build PATH for build script")] BuildScriptPath(#[source] env::JoinPathsError), - #[error("Failed to parse requirements from build backend")] - DirectUrl(#[source] Box), } #[derive(Debug)] @@ -244,7 +241,7 @@ pub struct Project { #[serde(rename_all = "kebab-case")] pub struct BuildSystem { /// PEP 508 dependencies required to execute the build system. - pub requires: Vec, + pub requires: Vec>, /// A string naming a Python object that will be used to perform the build. pub build_backend: Option, /// Specify that their backend code is hosted in-tree, this key contains a list of directories. @@ -601,9 +598,8 @@ impl SourceBuild { requirements: build_system .requires .into_iter() - .map(Requirement::from_pep508) - .collect::>() - .map_err(|err| Box::new(Error::DirectUrl(err)))?, + .map(Requirement::from) + .collect(), } } else { // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with @@ -982,7 +978,7 @@ async fn create_pep517_build_environment( })?; // Deserialize the requirements from the output file. - let extra_requires: Vec = serde_json::from_slice::>(&contents).map_err(|err| { + let extra_requires: Vec> = serde_json::from_slice::>>(&contents).map_err(|err| { Error::from_command_output( format!( "Build backend failed to return extra requires with `get_requires_for_build_{build_kind}`: {err}" @@ -991,11 +987,7 @@ async fn create_pep517_build_environment( version_id, ) })?; - let extra_requires: Vec<_> = extra_requires - .into_iter() - .map(Requirement::from_pep508) - .collect::>() - .map_err(Error::DirectUrl)?; + let extra_requires: Vec<_> = extra_requires.into_iter().map(Requirement::from).collect(); // Some packages (such as tqdm 4.66.1) list only extra requires that have already been part of // the pyproject.toml requires (in this case, `wheel`). We can skip doing the whole resolution diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 9ae242f71..01c706d8f 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -20,6 +20,7 @@ distribution-filename = { workspace = true } distribution-types = { workspace = true } install-wheel-rs = { workspace = true } pep508_rs = { workspace = true } +pypi-types = { workspace = true } uv-build = { workspace = true } uv-cache = { workspace = true, features = ["clap"] } uv-client = { workspace = true } diff --git a/crates/uv-dev/src/wheel_metadata.rs b/crates/uv-dev/src/wheel_metadata.rs index fd4a768b4..c270e17e1 100644 --- a/crates/uv-dev/src/wheel_metadata.rs +++ b/crates/uv-dev/src/wheel_metadata.rs @@ -5,8 +5,9 @@ use anyhow::{bail, Result}; use clap::Parser; use distribution_filename::WheelFilename; -use distribution_types::{BuiltDist, DirectUrlBuiltDist, ParsedUrl, RemoteSource}; +use distribution_types::{BuiltDist, DirectUrlBuiltDist, RemoteSource}; use pep508_rs::VerbatimUrl; +use pypi_types::ParsedUrl; use uv_cache::{Cache, CacheArgs}; use uv_client::RegistryClientBuilder; diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index b318f6232..9353798db 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -4,7 +4,6 @@ use tokio::task::JoinError; use zip::result::ZipError; use distribution_filename::WheelFilenameError; -use distribution_types::ParsedUrlError; use pep440_rs::Version; use pypi_types::HashDigest; use uv_client::BetterReqwestError; @@ -28,8 +27,6 @@ pub enum Error { #[error("Git operation failed")] Git(#[source] anyhow::Error), #[error(transparent)] - DirectUrl(#[from] Box), - #[error(transparent)] Reqwest(#[from] BetterReqwestError), #[error(transparent)] Client(#[from] uv_client::Error), diff --git a/crates/uv-distribution/src/git.rs b/crates/uv-distribution/src/git.rs index abe4bc35d..a35bfeb2c 100644 --- a/crates/uv-distribution/src/git.rs +++ b/crates/uv-distribution/src/git.rs @@ -8,7 +8,7 @@ use tracing::debug; use url::Url; use cache_key::{CanonicalUrl, RepositoryUrl}; -use distribution_types::ParsedGitUrl; +use pypi_types::ParsedGitUrl; use uv_cache::{Cache, CacheBucket}; use uv_fs::LockedFile; use uv_git::{Fetch, GitReference, GitSha, GitSource, GitUrl}; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 324cdba61..493ba8332 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -17,12 +17,11 @@ use zip::ZipArchive; use distribution_filename::WheelFilename; use distribution_types::{ BuildableSource, DirectorySourceDist, DirectorySourceUrl, Dist, FileLocation, GitSourceUrl, - HashPolicy, Hashed, LocalEditable, ParsedArchiveUrl, PathSourceUrl, RemoteSource, SourceDist, - SourceUrl, + HashPolicy, Hashed, LocalEditable, PathSourceUrl, RemoteSource, SourceDist, SourceUrl, }; use install_wheel_rs::metadata::read_archive_metadata; use platform_tags::Tags; -use pypi_types::{HashDigest, Metadata23}; +use pypi_types::{HashDigest, Metadata23, ParsedArchiveUrl}; use uv_cache::{ ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, Timestamp, WheelCache, @@ -1026,7 +1025,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Resolve to a precise Git SHA. let url = if let Some(url) = resolve_precise( - &resource.git, + resource.git, self.build_context.cache(), self.reporter.as_ref(), ) @@ -1034,11 +1033,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { { Cow::Owned(url) } else { - Cow::Borrowed(resource.git.as_ref()) + Cow::Borrowed(resource.git) }; - let subdirectory = resource.subdirectory.as_deref(); - // Fetch the Git repository. let fetch = fetch_git_archive(&url, self.build_context.cache(), self.reporter.as_ref()).await?; @@ -1062,7 +1059,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map(|reporter| reporter.on_build_start(source)); let (disk_filename, filename, metadata) = self - .build_distribution(source, fetch.path(), subdirectory, &cache_shard) + .build_distribution(source, fetch.path(), resource.subdirectory, &cache_shard) .await?; if let Some(task) = task { @@ -1102,7 +1099,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Resolve to a precise Git SHA. let url = if let Some(url) = resolve_precise( - &resource.git, + resource.git, self.build_context.cache(), self.reporter.as_ref(), ) @@ -1110,11 +1107,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { { Cow::Owned(url) } else { - Cow::Borrowed(resource.git.as_ref()) + Cow::Borrowed(resource.git) }; - let subdirectory = resource.subdirectory.as_deref(); - // Fetch the Git repository. let fetch = fetch_git_archive(&url, self.build_context.cache(), self.reporter.as_ref()).await?; @@ -1143,7 +1138,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self - .build_metadata(source, fetch.path(), subdirectory) + .build_metadata(source, fetch.path(), resource.subdirectory) .boxed_local() .await? { @@ -1165,7 +1160,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map(|reporter| reporter.on_build_start(source)); let (_disk_filename, _filename, metadata) = self - .build_distribution(source, fetch.path(), subdirectory, &cache_shard) + .build_distribution(source, fetch.path(), resource.subdirectory, &cache_shard) .await?; if let Some(task) = task { diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 38e4db8ee..8f2176919 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -12,6 +12,7 @@ use distribution_types::{ UnresolvedRequirementSpecification, }; use pep440_rs::{Version, VersionSpecifiers}; +use pypi_types::VerbatimParsedUrl; use requirements_txt::EditableRequirement; use uv_cache::{ArchiveTarget, ArchiveTimestamp}; use uv_interpreter::PythonEnvironment; @@ -341,9 +342,9 @@ impl SitePackages { &requirement.extras, ) { let dependency = UnresolvedRequirementSpecification { - requirement: UnresolvedRequirement::Named( - Requirement::from_pep508(dependency)?, - ), + requirement: UnresolvedRequirement::Named(Requirement::from( + dependency, + )), hashes: vec![], }; if seen.insert(dependency.clone()) { @@ -363,7 +364,9 @@ impl SitePackages { while let Some(entry) = stack.pop() { let installed = match &entry.requirement { UnresolvedRequirement::Named(requirement) => self.get_packages(&requirement.name), - UnresolvedRequirement::Unnamed(requirement) => self.get_urls(requirement.url.raw()), + UnresolvedRequirement::Unnamed(requirement) => { + self.get_urls(requirement.url.verbatim.raw()) + } }; match installed.as_slice() { [] => { @@ -373,7 +376,7 @@ impl SitePackages { [distribution] => { match RequirementSatisfaction::check( distribution, - entry.requirement.source()?.as_ref(), + entry.requirement.source().as_ref(), )? { RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => { return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())) @@ -405,9 +408,9 @@ impl SitePackages { entry.requirement.extras(), ) { let dependency = UnresolvedRequirementSpecification { - requirement: UnresolvedRequirement::Named( - Requirement::from_pep508(dependency)?, - ), + requirement: UnresolvedRequirement::Named(Requirement::from( + dependency, + )), hashes: vec![], }; if seen.insert(dependency.clone()) { @@ -471,7 +474,7 @@ pub enum SitePackagesDiagnostic { /// The package that is missing a dependency. package: PackageName, /// The dependency that is missing. - requirement: pep508_rs::Requirement, + requirement: pep508_rs::Requirement, }, IncompatibleDependency { /// The package that has an incompatible dependency. @@ -479,7 +482,7 @@ pub enum SitePackagesDiagnostic { /// The version of the package that is installed. version: Version, /// The dependency that is incompatible. - requirement: pep508_rs::Requirement, + requirement: pep508_rs::Requirement, }, DuplicatePackage { /// The package that has multiple installed distributions. diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 1469ac9f8..492f07765 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -24,8 +24,6 @@ pub enum LookaheadError { DownloadAndBuild(SourceDist, #[source] uv_distribution::Error), #[error(transparent)] UnsupportedUrl(#[from] distribution_types::Error), - #[error(transparent)] - InvalidRequirement(#[from] Box), } /// A resolver for resolving lookahead requirements from direct URLs. @@ -211,8 +209,8 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> { .requires_dist .iter() .cloned() - .map(Requirement::from_pep508) - .collect::>()? + .map(Requirement::from) + .collect() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let archive = self @@ -233,10 +231,7 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> { .distributions() .done(id, Arc::new(MetadataResponse::Found(archive))); - requires_dist - .into_iter() - .map(Requirement::from_pep508) - .collect::>()? + requires_dist.into_iter().map(Requirement::from).collect() } }; diff --git a/crates/uv-requirements/src/pyproject.rs b/crates/uv-requirements/src/pyproject.rs index abd7131a7..f616fb63a 100644 --- a/crates/uv-requirements/src/pyproject.rs +++ b/crates/uv-requirements/src/pyproject.rs @@ -20,9 +20,10 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; -use distribution_types::{ParsedUrlError, Requirement, RequirementSource, Requirements}; +use distribution_types::{Requirement, RequirementSource, Requirements}; use pep440_rs::VersionSpecifiers; -use pep508_rs::{RequirementOrigin, VerbatimUrl, VersionOrUrl}; +use pep508_rs::{Pep508Error, RequirementOrigin, VerbatimUrl, VersionOrUrl}; +use pypi_types::VerbatimParsedUrl; use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_git::GitReference; @@ -34,7 +35,7 @@ use crate::ExtrasSpecification; #[derive(Debug, Error)] pub enum Pep621Error { #[error(transparent)] - Pep508(#[from] pep508_rs::Pep508Error), + Pep508(#[from] Box>), #[error("Must specify a `[project]` section alongside `[tool.uv.sources]`")] MissingProjectSection, #[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")] @@ -43,12 +44,16 @@ pub enum Pep621Error { LoweringError(PackageName, #[source] LoweringError), } +impl From> for Pep621Error { + fn from(error: Pep508Error) -> Self { + Self::Pep508(Box::new(error)) + } +} + /// An error parsing and merging `tool.uv.sources` with /// `project.{dependencies,optional-dependencies}`. #[derive(Debug, Error)] pub enum LoweringError { - #[error("Invalid URL structure")] - DirectUrl(#[from] Box), #[error("Unsupported path (can't convert to URL): `{}`", _0.user_display())] PathToUrl(PathBuf), #[error("Package is not included as workspace package in `tool.uv.workspace`")] @@ -385,7 +390,7 @@ pub(crate) fn lower_requirements( /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`. pub(crate) fn lower_requirement( - requirement: pep508_rs::Requirement, + requirement: pep508_rs::Requirement, project_name: &PackageName, project_dir: &Path, project_sources: &BTreeMap, @@ -420,7 +425,7 @@ pub(crate) fn lower_requirement( requirement.name ); } - return Ok(Requirement::from_pep508(requirement)?); + return Ok(Requirement::from(requirement)); }; if preview.is_disabled() { diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 548b16411..da181535b 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -11,6 +11,7 @@ use distribution_types::{ BuildableSource, DirectorySourceUrl, HashPolicy, Requirement, SourceUrl, VersionId, }; use pep508_rs::RequirementOrigin; +use pypi_types::VerbatimParsedUrl; use uv_distribution::{DistributionDatabase, Reporter}; use uv_fs::Simplified; use uv_resolver::{InMemoryIndex, MetadataResponse}; @@ -74,12 +75,15 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { Ok(requirements .into_iter() .flatten() - .map(Requirement::from_pep508) - .collect::>()?) + .map(Requirement::from) + .collect()) } /// Infer the package name for a given "unnamed" requirement. - async fn resolve_source_tree(&self, path: &Path) -> Result> { + async fn resolve_source_tree( + &self, + path: &Path, + ) -> Result>> { // Convert to a buildable source. let source_tree = fs_err::canonicalize(path).with_context(|| { format!( diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index c66656d06..aab281105 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -11,7 +11,8 @@ use distribution_types::{ FlatIndexLocation, IndexUrl, Requirement, RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification, }; -use pep508_rs::{UnnamedRequirement, VerbatimUrl}; +use pep508_rs::{UnnamedRequirement, UnnamedRequirementUrl}; +use pypi_types::VerbatimParsedUrl; use requirements_txt::{ EditableRequirement, FindLink, RequirementEntry, RequirementsTxt, RequirementsTxtRequirement, }; @@ -67,12 +68,12 @@ impl RequirementsSpecification { let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?) .with_context(|| format!("Failed to parse: `{name}`"))?; Self { - requirements: vec![UnresolvedRequirementSpecification::try_from( + requirements: vec![UnresolvedRequirementSpecification::from( RequirementEntry { requirement, hashes: vec![], }, - )?], + )], ..Self::default() } } @@ -96,8 +97,8 @@ impl RequirementsSpecification { constraints: requirements_txt .constraints .into_iter() - .map(Requirement::from_pep508) - .collect::>()?, + .map(Requirement::from) + .collect(), editables: requirements_txt.editables, index_url: requirements_txt.index_url.map(IndexUrl::from), extra_index_urls: requirements_txt @@ -132,7 +133,7 @@ impl RequirementsSpecification { project: None, requirements: vec![UnresolvedRequirementSpecification { requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement { - url: VerbatimUrl::from_path(path)?, + url: VerbatimParsedUrl::parse_absolute_path(path)?, extras: vec![], marker: None, origin: None, diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 94b0d270b..d07e3cb84 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -11,12 +11,12 @@ use tracing::debug; use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_types::{ - BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, ParsedGitUrl, - PathSourceUrl, RemoteSource, Requirement, SourceUrl, UnresolvedRequirement, + BuildableSource, DirectSourceUrl, DirectorySourceUrl, GitSourceUrl, PathSourceUrl, + RemoteSource, Requirement, SourceUrl, UnresolvedRequirement, UnresolvedRequirementSpecification, VersionId, }; -use pep508_rs::{Scheme, UnnamedRequirement, VersionOrUrl}; -use pypi_types::Metadata10; +use pep508_rs::{UnnamedRequirement, VersionOrUrl}; +use pypi_types::{Metadata10, ParsedUrl, VerbatimParsedUrl}; use uv_distribution::{DistributionDatabase, Reporter}; use uv_normalize::PackageName; use uv_resolver::{InMemoryIndex, MetadataResponse}; @@ -72,9 +72,9 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { .map(|entry| async { match entry.requirement { UnresolvedRequirement::Named(requirement) => Ok(requirement), - UnresolvedRequirement::Unnamed(requirement) => Ok(Requirement::from_pep508( + UnresolvedRequirement::Unnamed(requirement) => Ok(Requirement::from( Self::resolve_requirement(requirement, hasher, index, &database).await?, - )?), + )), } }) .collect::>() @@ -84,19 +84,19 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { /// Infer the package name for a given "unnamed" requirement. async fn resolve_requirement( - requirement: UnnamedRequirement, + requirement: UnnamedRequirement, hasher: &HashStrategy, index: &InMemoryIndex, database: &DistributionDatabase<'a, Context>, - ) -> Result { + ) -> Result> { // If the requirement is a wheel, extract the package name from the wheel filename. // // Ex) `anyio-4.3.0-py3-none-any.whl` - if Path::new(requirement.url.path()) + if Path::new(requirement.url.verbatim.path()) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) { - let filename = WheelFilename::from_str(&requirement.url.filename()?)?; + let filename = WheelFilename::from_str(&requirement.url.verbatim.filename()?)?; return Ok(pep508_rs::Requirement { name: filename.name, extras: requirement.extras, @@ -112,6 +112,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { // Ex) `anyio-4.3.0.tar.gz` if let Some(filename) = requirement .url + .verbatim .filename() .ok() .and_then(|filename| SourceDistFilename::parsed_normalized_filename(&filename).ok()) @@ -125,27 +126,43 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { }); } - let source = match Scheme::parse(requirement.url.scheme()) { - Some(Scheme::File) => { - let path = requirement - .url - .to_file_path() - .expect("URL to be a file path"); + let source = match &requirement.url.parsed_url { + // If the path points to a directory, attempt to read the name from static metadata. + ParsedUrl::Path(parsed_path_url) if parsed_path_url.path.is_dir() => { + // Attempt to read a `PKG-INFO` from the directory. + if let Some(metadata) = fs_err::read(parsed_path_url.path.join("PKG-INFO")) + .ok() + .and_then(|contents| Metadata10::parse_pkg_info(&contents).ok()) + { + debug!( + "Found PKG-INFO metadata for {path} ({name})", + path = parsed_path_url.path.display(), + name = metadata.name + ); + return Ok(pep508_rs::Requirement { + name: metadata.name, + extras: requirement.extras, + version_or_url: Some(VersionOrUrl::Url(requirement.url)), + marker: requirement.marker, + origin: requirement.origin, + }); + } - // If the path points to a directory, attempt to read the name from static metadata. - if path.is_dir() { - // Attempt to read a `PKG-INFO` from the directory. - if let Some(metadata) = fs_err::read(path.join("PKG-INFO")) - .ok() - .and_then(|contents| Metadata10::parse_pkg_info(&contents).ok()) - { + // Attempt to read a `pyproject.toml` file. + let project_path = parsed_path_url.path.join("pyproject.toml"); + if let Some(pyproject) = fs_err::read_to_string(project_path) + .ok() + .and_then(|contents| toml::from_str::(&contents).ok()) + { + // Read PEP 621 metadata from the `pyproject.toml`. + if let Some(project) = pyproject.project { debug!( - "Found PKG-INFO metadata for {path} ({name})", - path = path.display(), - name = metadata.name + "Found PEP 621 metadata for {path} in `pyproject.toml` ({name})", + path = parsed_path_url.path.display(), + name = project.name ); return Ok(pep508_rs::Requirement { - name: metadata.name, + name: project.name, extras: requirement.extras, version_or_url: Some(VersionOrUrl::Url(requirement.url)), marker: requirement.marker, @@ -153,106 +170,75 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { }); } - // Attempt to read a `pyproject.toml` file. - let project_path = path.join("pyproject.toml"); - if let Some(pyproject) = fs_err::read_to_string(project_path) - .ok() - .and_then(|contents| toml::from_str::(&contents).ok()) - { - // Read PEP 621 metadata from the `pyproject.toml`. - if let Some(project) = pyproject.project { - debug!( - "Found PEP 621 metadata for {path} in `pyproject.toml` ({name})", - path = path.display(), - name = project.name - ); - return Ok(pep508_rs::Requirement { - name: project.name, - extras: requirement.extras, - version_or_url: Some(VersionOrUrl::Url(requirement.url)), - marker: requirement.marker, - origin: requirement.origin, - }); - } - - // Read Poetry-specific metadata from the `pyproject.toml`. - if let Some(tool) = pyproject.tool { - if let Some(poetry) = tool.poetry { - if let Some(name) = poetry.name { - debug!( - "Found Poetry metadata for {path} in `pyproject.toml` ({name})", - path = path.display(), - name = name - ); - return Ok(pep508_rs::Requirement { - name, - extras: requirement.extras, - version_or_url: Some(VersionOrUrl::Url(requirement.url)), - marker: requirement.marker, - origin: requirement.origin, - }); - } + // Read Poetry-specific metadata from the `pyproject.toml`. + if let Some(tool) = pyproject.tool { + if let Some(poetry) = tool.poetry { + if let Some(name) = poetry.name { + debug!( + "Found Poetry metadata for {path} in `pyproject.toml` ({name})", + path = parsed_path_url.path.display(), + name = name + ); + return Ok(pep508_rs::Requirement { + name, + extras: requirement.extras, + version_or_url: Some(VersionOrUrl::Url(requirement.url)), + marker: requirement.marker, + origin: requirement.origin, + }); } } } + } - // Attempt to read a `setup.cfg` from the directory. - if let Some(setup_cfg) = fs_err::read_to_string(path.join("setup.cfg")) + // Attempt to read a `setup.cfg` from the directory. + if let Some(setup_cfg) = + fs_err::read_to_string(parsed_path_url.path.join("setup.cfg")) .ok() .and_then(|contents| { let mut ini = Ini::new_cs(); ini.set_multiline(true); ini.read(contents).ok() }) - { - if let Some(section) = setup_cfg.get("metadata") { - if let Some(Some(name)) = section.get("name") { - if let Ok(name) = PackageName::from_str(name) { - debug!( - "Found setuptools metadata for {path} in `setup.cfg` ({name})", - path = path.display(), - name = name - ); - return Ok(pep508_rs::Requirement { - name, - extras: requirement.extras, - version_or_url: Some(VersionOrUrl::Url(requirement.url)), - marker: requirement.marker, - origin: requirement.origin, - }); - } + { + if let Some(section) = setup_cfg.get("metadata") { + if let Some(Some(name)) = section.get("name") { + if let Ok(name) = PackageName::from_str(name) { + debug!( + "Found setuptools metadata for {path} in `setup.cfg` ({name})", + path = parsed_path_url.path.display(), + name = name + ); + return Ok(pep508_rs::Requirement { + name, + extras: requirement.extras, + version_or_url: Some(VersionOrUrl::Url(requirement.url)), + marker: requirement.marker, + origin: requirement.origin, + }); } } } - - SourceUrl::Directory(DirectorySourceUrl { - url: &requirement.url, - path: Cow::Owned(path), - }) - } else { - SourceUrl::Path(PathSourceUrl { - url: &requirement.url, - path: Cow::Owned(path), - }) } - } - Some(Scheme::Http | Scheme::Https) => SourceUrl::Direct(DirectSourceUrl { - url: &requirement.url, - }), - Some(Scheme::GitSsh | Scheme::GitHttps | Scheme::GitHttp) => { - let git = ParsedGitUrl::try_from(requirement.url.to_url())?; - SourceUrl::Git(GitSourceUrl { - git: Cow::Owned(git.url), - subdirectory: git.subdirectory.map(Cow::Owned), - url: &requirement.url, + + SourceUrl::Directory(DirectorySourceUrl { + url: &requirement.url.verbatim, + path: Cow::Borrowed(&parsed_path_url.path), }) } - _ => { - return Err(anyhow::anyhow!( - "Unsupported scheme for unnamed requirement: {}", - requirement.url - )); - } + // If it's not a directory, assume it's a file. + ParsedUrl::Path(parsed_path_url) => SourceUrl::Path(PathSourceUrl { + url: &requirement.url.verbatim, + path: Cow::Borrowed(&parsed_path_url.path), + }), + ParsedUrl::Archive(parsed_archive_url) => SourceUrl::Direct(DirectSourceUrl { + url: &parsed_archive_url.url, + }), + ParsedUrl::Git(parsed_git_url) => SourceUrl::Git(GitSourceUrl { + url: &requirement.url.verbatim, + git: &parsed_git_url.url, + subdirectory: parsed_git_url.subdirectory.as_deref(), + }), }; // Fetch the metadata for the distribution. diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index ebaf41f90..4e25d202a 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -9,7 +9,7 @@ use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter} use rustc_hash::{FxHashMap, FxHashSet}; use dashmap::DashMap; -use distribution_types::{BuiltDist, IndexLocations, InstalledDist, ParsedUrlError, SourceDist}; +use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist}; use pep440_rs::Version; use pep508_rs::Requirement; use uv_normalize::PackageName; @@ -96,10 +96,6 @@ pub enum ResolveError { #[error("In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `{0}`")] UnhashedPackage(PackageName), - // TODO(konsti): Attach the distribution that contained the invalid requirement as error source. - #[error("Failed to parse requirements")] - DirectUrl(#[from] Box), - /// Something unexpected happened. #[error("{0}")] Failure(String), diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 2f80e0b41..756fbb4cf 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -12,14 +12,13 @@ use url::Url; use distribution_filename::WheelFilename; use distribution_types::{ BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, FileLocation, - GitSourceDist, IndexUrl, ParsedArchiveUrl, ParsedGitUrl, PathBuiltDist, PathSourceDist, - RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, - ResolvedDist, ToUrlError, + GitSourceDist, IndexUrl, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, + RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError, }; use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, VerbatimUrl}; use platform_tags::{TagCompatibility, TagPriority, Tags}; -use pypi_types::HashDigest; +use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl}; use uv_git::{GitReference, GitSha}; use uv_normalize::PackageName; diff --git a/crates/uv-resolver/src/preferences.rs b/crates/uv-resolver/src/preferences.rs index 5f1aa636b..be6591a7d 100644 --- a/crates/uv-resolver/src/preferences.rs +++ b/crates/uv-resolver/src/preferences.rs @@ -4,21 +4,19 @@ use std::sync::Arc; use rustc_hash::FxHashMap; use tracing::trace; -use distribution_types::{ParsedUrlError, Requirement, RequirementSource}; +use distribution_types::{Requirement, RequirementSource}; use pep440_rs::{Operator, Version}; use pep508_rs::{MarkerEnvironment, UnnamedRequirement}; -use pypi_types::{HashDigest, HashError}; +use pypi_types::{HashDigest, HashError, VerbatimParsedUrl}; use requirements_txt::{RequirementEntry, RequirementsTxtRequirement}; use uv_normalize::PackageName; #[derive(thiserror::Error, Debug)] pub enum PreferenceError { #[error("direct URL requirements without package names are not supported: `{0}`")] - Bare(UnnamedRequirement), + Bare(UnnamedRequirement), #[error(transparent)] Hash(#[from] HashError), - #[error(transparent)] - ParsedUrl(#[from] Box), } /// A pinned requirement, as extracted from a `requirements.txt` file. @@ -33,9 +31,7 @@ impl Preference { pub fn from_entry(entry: RequirementEntry) -> Result { Ok(Self { requirement: match entry.requirement { - RequirementsTxtRequirement::Named(requirement) => { - Requirement::from_pep508(requirement)? - } + RequirementsTxtRequirement::Named(requirement) => Requirement::from(requirement), RequirementsTxtRequirement::Unnamed(requirement) => { return Err(PreferenceError::Bare(requirement)); } diff --git a/crates/uv-resolver/src/pubgrub/distribution.rs b/crates/uv-resolver/src/pubgrub/distribution.rs index cdb020295..3d3ec74c5 100644 --- a/crates/uv-resolver/src/pubgrub/distribution.rs +++ b/crates/uv-resolver/src/pubgrub/distribution.rs @@ -1,5 +1,6 @@ -use distribution_types::{DistributionMetadata, Name, VerbatimParsedUrl, VersionOrUrlRef}; +use distribution_types::{DistributionMetadata, Name, VersionOrUrlRef}; use pep440_rs::Version; +use pypi_types::VerbatimParsedUrl; use uv_normalize::PackageName; #[derive(Debug)] diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index 0867ac239..980cce160 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -1,5 +1,5 @@ -use distribution_types::VerbatimParsedUrl; use pep508_rs::MarkerTree; +use pypi_types::VerbatimParsedUrl; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::sync::Arc; diff --git a/crates/uv-resolver/src/redirect.rs b/crates/uv-resolver/src/redirect.rs index 47717bb25..714ec912b 100644 --- a/crates/uv-resolver/src/redirect.rs +++ b/crates/uv-resolver/src/redirect.rs @@ -1,7 +1,7 @@ use url::Url; -use distribution_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; use pep508_rs::VerbatimUrl; +use pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; use uv_distribution::git_url_to_precise; use uv_git::GitReference; diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index c40aba193..1df1fbe63 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -7,12 +7,12 @@ use pubgrub::type_aliases::SelectedDependencies; use rustc_hash::{FxHashMap, FxHashSet}; use distribution_types::{ - Dist, DistributionMetadata, Name, ParsedUrlError, Requirement, ResolutionDiagnostic, - ResolvedDist, VersionId, VersionOrUrlRef, + Dist, DistributionMetadata, Name, Requirement, ResolutionDiagnostic, ResolvedDist, VersionId, + VersionOrUrlRef, }; use pep440_rs::{Version, VersionSpecifier}; use pep508_rs::MarkerEnvironment; -use pypi_types::Yanked; +use pypi_types::{ParsedUrlError, Yanked}; use uv_normalize::PackageName; use crate::dependency_provider::UvDependencyProvider; @@ -512,8 +512,8 @@ impl ResolutionGraph { .requires_dist .iter() .cloned() - .map(Requirement::from_pep508) - .collect::>()?; + .map(Requirement::from) + .collect(); for req in manifest.apply(requirements.iter()) { let Some(ref marker_tree) = req.marker else { continue; diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs index dded38a7d..414df49c5 100644 --- a/crates/uv-resolver/src/resolver/locals.rs +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -203,9 +203,10 @@ mod tests { use anyhow::Result; use url::Url; - use distribution_types::{ParsedUrl, RequirementSource}; + use distribution_types::RequirementSource; use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifiers}; use pep508_rs::VerbatimUrl; + use pypi_types::ParsedUrl; use crate::resolver::locals::{iter_locals, Locals}; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 2507841b4..99a70bed5 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1063,8 +1063,8 @@ impl ResolverState>()?; + .map(Requirement::from) + .collect(); let dependencies = PubGrubDependencies::from_requirements( &requirements, &self.constraints, @@ -1170,8 +1170,8 @@ impl ResolverState>()?; + .map(Requirement::from) + .collect(); let dependencies = PubGrubDependencies::from_requirements( &requirements, &self.constraints, diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index 95867a3ee..1a7e6d66b 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -1,11 +1,9 @@ +use distribution_types::{RequirementSource, Verbatim}; use rustc_hash::FxHashMap; use tracing::debug; -use distribution_types::{ - ParsedArchiveUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, RequirementSource, Verbatim, - VerbatimParsedUrl, -}; use pep508_rs::{MarkerEnvironment, VerbatimUrl}; +use pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl}; use uv_distribution::is_same_reference; use uv_git::GitUrl; use uv_normalize::PackageName; diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index ec223dd63..20b629be8 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -167,10 +167,9 @@ macro_rules! assert_snapshot { #[tokio::test] async fn black() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .exclude_newer(Some(*EXCLUDE_NEWER)) .build(); @@ -196,10 +195,9 @@ async fn black() -> Result<()> { #[tokio::test] async fn black_colorama() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black[colorama]<=23.9.1").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .exclude_newer(Some(*EXCLUDE_NEWER)) .build(); @@ -228,10 +226,9 @@ async fn black_colorama() -> Result<()> { /// Resolve Black with an invalid extra. The resolver should ignore the extra. #[tokio::test] async fn black_tensorboard() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black[tensorboard]<=23.9.1").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .exclude_newer(Some(*EXCLUDE_NEWER)) .build(); @@ -257,10 +254,9 @@ async fn black_tensorboard() -> Result<()> { #[tokio::test] async fn black_python_310() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .exclude_newer(Some(*EXCLUDE_NEWER)) .build(); @@ -293,14 +289,12 @@ async fn black_python_310() -> Result<()> { #[tokio::test] async fn black_mypy_extensions() -> Result<()> { let manifest = Manifest::new( - vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("black<=23.9.1").unwrap()) - .unwrap(), - ], - Constraints::from_requirements(vec![Requirement::from_pep508( + vec![Requirement::from( + pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(), + )], + Constraints::from_requirements(vec![Requirement::from( pep508_rs::Requirement::from_str("mypy-extensions<0.4.4").unwrap(), - ) - .unwrap()]), + )]), Overrides::default(), vec![], None, @@ -336,14 +330,12 @@ async fn black_mypy_extensions() -> Result<()> { #[tokio::test] async fn black_mypy_extensions_extra() -> Result<()> { let manifest = Manifest::new( - vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("black<=23.9.1").unwrap()) - .unwrap(), - ], - Constraints::from_requirements(vec![Requirement::from_pep508( + vec![Requirement::from( + pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(), + )], + Constraints::from_requirements(vec![Requirement::from( pep508_rs::Requirement::from_str("mypy-extensions[extra]<0.4.4").unwrap(), - ) - .unwrap()]), + )]), Overrides::default(), vec![], None, @@ -379,14 +371,12 @@ async fn black_mypy_extensions_extra() -> Result<()> { #[tokio::test] async fn black_flake8() -> Result<()> { let manifest = Manifest::new( - vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("black<=23.9.1").unwrap()) - .unwrap(), - ], - Constraints::from_requirements(vec![Requirement::from_pep508( + vec![Requirement::from( + pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(), + )], + Constraints::from_requirements(vec![Requirement::from( pep508_rs::Requirement::from_str("flake8<1").unwrap(), - ) - .unwrap()]), + )]), Overrides::default(), vec![], None, @@ -419,10 +409,9 @@ async fn black_flake8() -> Result<()> { #[tokio::test] async fn black_lowest() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black>21").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .resolution_mode(ResolutionMode::Lowest) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -449,10 +438,9 @@ async fn black_lowest() -> Result<()> { #[tokio::test] async fn black_lowest_direct() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black>21").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .resolution_mode(ResolutionMode::LowestDirect) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -480,12 +468,14 @@ async fn black_lowest_direct() -> Result<()> { #[tokio::test] async fn black_respect_preference() -> Result<()> { let manifest = Manifest::new( - vec![Requirement::from_pep508(pep508_rs::Requirement::from_str("black<=23.9.1")?).unwrap()], + vec![Requirement::from(pep508_rs::Requirement::from_str( + "black<=23.9.1", + )?)], Constraints::default(), Overrides::default(), - vec![Preference::from_requirement( - Requirement::from_pep508(pep508_rs::Requirement::from_str("black==23.9.0")?).unwrap(), - )], + vec![Preference::from_requirement(Requirement::from( + pep508_rs::Requirement::from_str("black==23.9.0")?, + ))], None, vec![], Exclusions::default(), @@ -518,12 +508,14 @@ async fn black_respect_preference() -> Result<()> { #[tokio::test] async fn black_ignore_preference() -> Result<()> { let manifest = Manifest::new( - vec![Requirement::from_pep508(pep508_rs::Requirement::from_str("black<=23.9.1")?).unwrap()], + vec![Requirement::from(pep508_rs::Requirement::from_str( + "black<=23.9.1", + )?)], Constraints::default(), Overrides::default(), - vec![Preference::from_requirement( - Requirement::from_pep508(pep508_rs::Requirement::from_str("black==23.9.2")?).unwrap(), - )], + vec![Preference::from_requirement(Requirement::from( + pep508_rs::Requirement::from_str("black==23.9.2")?, + ))], None, vec![], Exclusions::default(), @@ -554,10 +546,9 @@ async fn black_ignore_preference() -> Result<()> { #[tokio::test] async fn black_disallow_prerelease() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black<=20.0").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::Disallow) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -578,10 +569,9 @@ async fn black_disallow_prerelease() -> Result<()> { #[tokio::test] async fn black_allow_prerelease_if_necessary() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("black<=20.0").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::IfNecessary) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -602,10 +592,9 @@ async fn black_allow_prerelease_if_necessary() -> Result<()> { #[tokio::test] async fn pylint_disallow_prerelease() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::Disallow) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -628,10 +617,9 @@ async fn pylint_disallow_prerelease() -> Result<()> { #[tokio::test] async fn pylint_allow_prerelease() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::Allow) .exclude_newer(Some(*EXCLUDE_NEWER)) @@ -655,10 +643,8 @@ async fn pylint_allow_prerelease() -> Result<()> { #[tokio::test] async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> { let manifest = Manifest::simple(vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()) - .unwrap(), - Requirement::from_pep508(pep508_rs::Requirement::from_str("isort>=5.0.0").unwrap()) - .unwrap(), + Requirement::from(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()), + Requirement::from(pep508_rs::Requirement::from_str("isort>=5.0.0").unwrap()), ]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::Explicit) @@ -683,10 +669,8 @@ async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> { #[tokio::test] async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> { let manifest = Manifest::simple(vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()) - .unwrap(), - Requirement::from_pep508(pep508_rs::Requirement::from_str("isort>=5.0.0b").unwrap()) - .unwrap(), + Requirement::from(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()), + Requirement::from(pep508_rs::Requirement::from_str("isort>=5.0.0b").unwrap()), ]); let options = OptionsBuilder::new() .prerelease_mode(PreReleaseMode::Explicit) @@ -712,10 +696,9 @@ async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> { /// fail with a pre-release-centric hint. #[tokio::test] async fn msgraph_sdk() -> Result<()> { - let manifest = Manifest::simple(vec![Requirement::from_pep508( + let manifest = Manifest::simple(vec![Requirement::from( pep508_rs::Requirement::from_str("msgraph-sdk==1.0.0").unwrap(), - ) - .unwrap()]); + )]); let options = OptionsBuilder::new() .exclude_newer(Some(*EXCLUDE_NEWER)) .build(); diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index e00262636..8afa11349 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -110,7 +110,7 @@ impl HashStrategy { } UnresolvedRequirement::Unnamed(requirement) => { // Direct URLs are always allowed. - PackageId::from_url(&requirement.url) + PackageId::from_url(&requirement.url.verbatim) } }; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 16ee1206f..e7cc0c05a 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -19,6 +19,7 @@ install-wheel-rs = { workspace = true, features = ["clap"], default-features = f pep440_rs = { workspace = true } pep508_rs = { workspace = true } platform-tags = { workspace = true } +pypi-types = { workspace = true } requirements-txt = { workspace = true, features = ["http"] } uv-auth = { workspace = true } uv-cache = { workspace = true, features = ["clap"] } diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 922b5e638..32dbad9a9 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -16,8 +16,7 @@ use tempfile::tempdir_in; use tracing::debug; use distribution_types::{ - IndexLocations, LocalEditable, LocalEditables, ParsedUrlError, SourceAnnotation, - SourceAnnotations, Verbatim, + IndexLocations, LocalEditable, LocalEditables, SourceAnnotation, SourceAnnotations, Verbatim, }; use distribution_types::{Requirement, Requirements}; use install_wheel_rs::linker::LinkMode; @@ -472,17 +471,17 @@ pub(crate) async fn pip_compile( .requires_dist .iter() .cloned() - .map(Requirement::from_pep508) - .collect::>()?, + .map(Requirement::from) + .collect(), optional_dependencies: IndexMap::default(), }; - Ok::<_, Box>(BuiltEditableMetadata { + BuiltEditableMetadata { built: built_editable.editable, metadata: built_editable.metadata, requirements, - }) + } }) - .collect::>()?; + .collect(); // Validate that the editables are compatible with the target Python version. for editable in &editables { diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 6b8203c2f..1ba3acb0e 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -2,12 +2,12 @@ use std::borrow::Cow; use std::fmt::Write; use anstream::eprint; +use distribution_types::{IndexLocations, Resolution}; use fs_err as fs; use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, enabled, Level}; -use distribution_types::{IndexLocations, Resolution}; use install_wheel_rs::linker::LinkMode; use platform_tags::Tags; use uv_auth::store_credentials_from_url; diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 80a095aca..4cd1af6d6 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -1,5 +1,6 @@ //! Common operations shared across the `pip` API and subcommands. +use pypi_types::{ParsedUrl, ParsedUrlError}; use std::fmt::Write; use std::path::PathBuf; @@ -14,7 +15,7 @@ use distribution_types::{ }; use distribution_types::{ DistributionMetadata, IndexLocations, InstalledMetadata, InstalledVersion, LocalDist, Name, - ParsedUrl, RequirementSource, Resolution, + RequirementSource, Resolution, }; use install_wheel_rs::linker::LinkMode; use pep440_rs::{VersionSpecifier, VersionSpecifiers}; @@ -177,7 +178,7 @@ pub(crate) async fn resolve( let python_requirement = PythonRequirement::from_marker_environment(interpreter, markers); // Map the editables to their metadata. - let editables = editables.as_metadata().map_err(Error::ParsedUrl)?; + let editables = editables.as_metadata(); // Determine any lookahead requirements. let lookaheads = match options.dependency_mode { @@ -769,12 +770,9 @@ pub(crate) enum Error { #[error(transparent)] Lookahead(#[from] uv_requirements::LookaheadError), - #[error(transparent)] - ParsedUrl(Box), - #[error(transparent)] Anyhow(#[from] anyhow::Error), #[error("Installed distribution has unsupported type")] - UnsupportedInstalledDist(#[source] Box), + UnsupportedInstalledDist(#[source] Box), } diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs index c83a66bcb..f008a5d83 100644 --- a/crates/uv/src/commands/pip/uninstall.rs +++ b/crates/uv/src/commands/pip/uninstall.rs @@ -7,6 +7,7 @@ use tracing::debug; use distribution_types::{InstalledMetadata, Name, Requirement, UnresolvedRequirement}; use pep508_rs::UnnamedRequirement; +use pypi_types::VerbatimParsedUrl; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{KeyringProviderType, PreviewMode}; @@ -94,7 +95,7 @@ pub(crate) async fn pip_uninstall( let site_packages = uv_installer::SitePackages::from_executable(&venv)?; // Partition the requirements into named and unnamed requirements. - let (named, unnamed): (Vec, Vec) = spec + let (named, unnamed): (Vec, Vec>) = spec .requirements .into_iter() .partition_map(|entry| match entry.requirement { @@ -118,7 +119,7 @@ pub(crate) async fn pip_uninstall( let urls = { let mut urls = unnamed .into_iter() - .map(|requirement| requirement.url.to_url()) + .map(|requirement| requirement.url.verbatim.to_url()) .collect::>(); urls.sort_unstable(); urls.dedup(); diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 06129124d..be4194f17 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -225,16 +225,14 @@ async fn venv_impl( let requirements = if interpreter.python_tuple() < (3, 12) { // Only include `setuptools` and `wheel` on Python <3.12 vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("pip").unwrap()).unwrap(), - Requirement::from_pep508(pep508_rs::Requirement::from_str("setuptools").unwrap()) - .unwrap(), - Requirement::from_pep508(pep508_rs::Requirement::from_str("wheel").unwrap()) - .unwrap(), + Requirement::from(pep508_rs::Requirement::from_str("pip").unwrap()), + Requirement::from(pep508_rs::Requirement::from_str("setuptools").unwrap()), + Requirement::from(pep508_rs::Requirement::from_str("wheel").unwrap()), ] } else { - vec![ - Requirement::from_pep508(pep508_rs::Requirement::from_str("pip").unwrap()).unwrap(), - ] + vec![Requirement::from( + pep508_rs::Requirement::from_str("pip").unwrap(), + )] }; // Resolve and install the requirements. diff --git a/crates/uv/src/editables.rs b/crates/uv/src/editables.rs index c0dfd31fa..9ac2e6bdc 100644 --- a/crates/uv/src/editables.rs +++ b/crates/uv/src/editables.rs @@ -6,7 +6,7 @@ use indexmap::IndexMap; use owo_colors::OwoColorize; use distribution_types::{ - InstalledDist, LocalEditable, LocalEditables, Name, ParsedUrlError, Requirement, Requirements, + InstalledDist, LocalEditable, LocalEditables, Name, Requirement, Requirements, }; use platform_tags::Tags; use requirements_txt::EditableRequirement; @@ -159,7 +159,7 @@ impl ResolvedEditables { }) } - pub(crate) fn as_metadata(&self) -> Result, Box> { + pub(crate) fn as_metadata(&self) -> Vec { self.iter() .map(|editable| { let dependencies: Vec<_> = editable @@ -167,16 +167,16 @@ impl ResolvedEditables { .requires_dist .iter() .cloned() - .map(Requirement::from_pep508) - .collect::>()?; - Ok::<_, Box>(BuiltEditableMetadata { + .map(Requirement::from) + .collect(); + BuiltEditableMetadata { built: editable.local().clone(), metadata: editable.metadata().clone(), requirements: Requirements { dependencies, optional_dependencies: IndexMap::default(), }, - }) + } }) .collect() } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 6149f3291..eeae2cbb6 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -5458,7 +5458,10 @@ fn unsupported_scheme() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Unsupported URL prefix `bzr` in URL: `bzr+https://example.com/anyio` (Bazaar is not supported) + error: Couldn't parse requirement in `requirements.in` at position 0 + Caused by: Unsupported URL prefix `bzr` in URL: `bzr+https://example.com/anyio` (Bazaar is not supported) + anyio @ bzr+https://example.com/anyio + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "### );