Use VerbatimParsedUrl in pep508_rs (#3758)

When parsing requirements from any source, directly parse the url parts
(and reject unsupported urls) instead of parsing url parts at a later
stage. This removes a bunch of error branches and concludes the work
parsing url parts once and passing them around everywhere.

Many usages of the assembled `VerbatimUrl` remain, but these can be
removed incrementally.

Please review commit-by-commit.
This commit is contained in:
konsti 2024-05-23 21:52:47 +02:00 committed by GitHub
parent 0d2f3fc4e4
commit 4db468e27f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 877 additions and 656 deletions

9
Cargo.lock generated
View file

@ -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",

View file

@ -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<WallTime>) {
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<WallTime>) {
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;

View file

@ -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 }

View file

@ -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<Cow<'a, Path>>,
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(),
}
}
}

View file

@ -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.

View file

@ -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;

View file

@ -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<pep508_rs::Requirement<VerbatimParsedUrl>> for Requirement {
/// Convert a [`pep508_rs::Requirement`] to a [`Requirement`].
pub fn from_pep508(requirement: pep508_rs::Requirement) -> Result<Self, Box<ParsedUrlError>> {
fn from(requirement: pep508_rs::Requirement<VerbatimParsedUrl>) -> 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,
})
}
}
}

View file

@ -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<VerbatimParsedUrl>),
}
impl Display for UnresolvedRequirement {
@ -64,17 +64,13 @@ impl UnresolvedRequirement {
}
/// Return the version specifier or URL for the requirement.
pub fn source(&self) -> Result<Cow<'_, RequirementSource>, Box<ParsedUrlError>> {
// 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(),
)),
}
}
}

View file

@ -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<T: Pep508Url> Display for Pep508Error<T> {
}
}
/// We need this to allow e.g. anyhow's `.context()`
/// We need this to allow anyhow's `.context()` and `AsDynError`.
impl<E: Error + Debug, T: Pep508Url<Err = E>> std::error::Error for Pep508Error<T> {}
#[cfg(feature = "pyo3")]
@ -155,17 +154,6 @@ pub struct Requirement<T: Pep508Url = VerbatimUrl> {
pub origin: Option<RequirementOrigin>,
}
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<T: Pep508Url + Display> Display for Requirement<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
@ -453,10 +441,19 @@ impl<T: Pep508Url> Requirement<T> {
..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 @ <url>` 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::<VerbatimUrl>::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::<VerbatimUrl>::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,8 +1261,9 @@ 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]")
let numpy = crate::UnnamedRequirement::<VerbatimUrl>::from_str(
"/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]",
)
.unwrap();
assert_eq!(
numpy.url.to_string(),
@ -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::<VerbatimUrl>::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::<Url>(&mut Cursor::new(marker), &mut TracingReporter).unwrap();
parse_markers_cursor::<VerbatimUrl>(&mut Cursor::new(marker), &mut TracingReporter)
.unwrap();
let expected = MarkerTree::And(vec![
MarkerTree::Expression(MarkerExpression::Version {
key: MarkerValueVersion::PythonVersion,

View file

@ -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<T: Pep508Url>(markers: &str) -> Result<Self, Pep508Error<T>> {
parse_markers(markers, &mut TracingReporter)
}
/// Parse a [`MarkerTree`] from a string with the given reporter.
pub fn parse_reporter(
markers: &str,

View file

@ -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<Path>, working_dir: impl AsRef<Path>)
-> Result<Self, Self::Err>;
/// Parse a URL from an absolute path.
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err>;
/// Parse a URL from a string.
fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err>;
/// Set the verbatim representation of the URL.
#[must_use]
fn with_given(self, given: impl Into<String>) -> 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<Path>,
working_dir: impl AsRef<Path>,
) -> Result<Self, VerbatimUrlError> {
Self::parse_path(path, working_dir)
}
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> {
Self::parse_absolute_path(path)
}
fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err> {
Ok(Self::parse_url(given)?)
}
fn with_given(self, given: impl Into<String>) -> 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<Url: UnnamedRequirementUrl = VerbatimUrl> {
/// 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<ExtraName>,
@ -34,7 +80,7 @@ pub struct UnnamedRequirement {
pub origin: Option<RequirementOrigin>,
}
impl UnnamedRequirement {
impl<Url: UnnamedRequirementUrl> UnnamedRequirement<Url> {
/// 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<Path>,
reporter: &mut impl Reporter,
) -> Result<Self, Pep508Error<Url>> {
parse_unnamed_requirement(
&mut Cursor::new(input),
Some(working_dir.as_ref()),
reporter,
)
}
}
impl Display for UnnamedRequirement {
impl<Url: UnnamedRequirementUrl> Display for UnnamedRequirement<Url> {
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 {
}
}
/// <https://github.com/serde-rs/serde/issues/908#issuecomment-298027413>
impl<'de> Deserialize<'de> for UnnamedRequirement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
impl Serialize for UnnamedRequirement {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl FromStr for UnnamedRequirement {
type Err = Pep508Error<VerbatimUrl>;
impl<Url: UnnamedRequirementUrl> FromStr for UnnamedRequirement<Url> {
type Err = Pep508Error<Url>;
/// Parse a PEP 508-like direct URL requirement without a package name.
fn from_str(input: &str) -> Result<Self, Self::Err> {
@ -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<Path>,
reporter: &mut impl Reporter,
) -> Result<Self, Pep508Error<VerbatimUrl>> {
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<Url: UnnamedRequirementUrl>(
cursor: &mut Cursor,
working_dir: Option<&Path>,
reporter: &mut impl Reporter,
) -> Result<UnnamedRequirement, Pep508Error<VerbatimUrl>> {
) -> Result<UnnamedRequirement<Url>, Pep508Error<Url>> {
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::<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: UnnamedRequirementUrl>(
url: &str,
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused))] working_dir: Option<&Path>,
cursor: &Cursor,
start: usize,
len: usize,
) -> Result<(VerbatimUrl, Vec<ExtraName>), Pep508Error<VerbatimUrl>> {
) -> Result<(Url, Vec<ExtraName>), Pep508Error<Url>> {
// 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::<VerbatimUrl>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<VerbatimUrl>::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<Url: UnnamedRequirementUrl>(
cursor: &mut Cursor,
working_dir: Option<&Path>,
) -> Result<(VerbatimUrl, Vec<ExtraName>), Pep508Error<VerbatimUrl>> {
) -> Result<(Url, Vec<ExtraName>), Pep508Error<Url>> {
// wsp*
cursor.eat_whitespace();
// <URI_reference>

View file

@ -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 }

View file

@ -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<Regex> = Lazy::new(|| Regex::new(r"(\d)([<>=~^!])").unwrap());
@ -114,7 +116,7 @@ fn parse_with_fixups<Err, T: FromStr<Err = Err>>(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<T: Pep508Url = VerbatimUrl>(Requirement<T>);
pub struct LenientRequirement<T: Pep508Url = VerbatimParsedUrl>(Requirement<T>);
impl<T: Pep508Url> FromStr for LenientRequirement<T> {
type Err = Pep508Error<T>;

View file

@ -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;

View file

@ -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
/// <https://packaging.python.org/specifications/core-metadata/>.
@ -29,7 +29,7 @@ pub struct Metadata23 {
pub name: PackageName,
pub version: Version,
// Optional fields
pub requires_dist: Vec<Requirement<VerbatimUrl>>,
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
pub requires_python: Option<VersionSpecifiers>,
pub provides_extras: Vec<ExtraName>,
}
@ -50,7 +50,7 @@ pub enum MetadataError {
#[error(transparent)]
Pep440Error(#[from] VersionSpecifiersParseError),
#[error(transparent)]
Pep508Error(#[from] Pep508Error<VerbatimUrl>),
Pep508Error(#[from] Box<Pep508Error<VerbatimParsedUrl>>),
#[error(transparent)]
InvalidName(#[from] InvalidNameError),
#[error("Invalid `Metadata-Version` field: {0}")]
@ -61,6 +61,12 @@ pub enum MetadataError {
DynamicField(&'static str),
}
impl From<Pep508Error<VerbatimParsedUrl>> for MetadataError {
fn from(error: Pep508Error<VerbatimParsedUrl>) -> Self {
Self::Pep508Error(Box::new(error))
}
}
/// From <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/metadata.rs#LL78C2-L91C26>
impl Metadata23 {
/// Parse the [`Metadata23`] from a `METADATA` file, as included in a built distribution (wheel).

View file

@ -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<Self, Self::Err> {
let verbatim_url = <VerbatimUrl as Pep508Url>::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<Path>,
working_dir: impl AsRef<Path>,
) -> Result<Self, Self::Err> {
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<Path>) -> Result<Self, Self::Err> {
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<str>) -> Result<Self, Self::Err> {
let verbatim = <VerbatimUrl as UnnamedRequirementUrl>::parse_unnamed_url(&url)?;
Ok(Self {
parsed_url: ParsedUrl::try_from(verbatim.to_url())?,
verbatim,
})
}
fn with_given(self, given: impl Into<String>) -> 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<VerbatimUrl> for VerbatimParsedUrl {
type Error = ParsedUrlError;
fn try_from(verbatim_url: VerbatimUrl) -> Result<Self, Self::Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
self.verbatim.serialize(serializer)
}
}
impl<'de> serde::de::Deserialize<'de> for VerbatimParsedUrl {
fn deserialize<D>(deserializer: D) -> Result<VerbatimParsedUrl, D::Error>
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<PathBuf> {
}
/// Return the Git reference of the given URL, if it exists.
pub fn git_reference(url: Url) -> Result<Option<GitSha>, Error> {
pub fn git_reference(url: Url) -> Result<Option<GitSha>, Box<ParsedUrlError>> {
let ParsedGitUrl { url, .. } = ParsedGitUrl::try_from(url)?;
Ok(url.precise())
}
@ -172,10 +275,10 @@ impl TryFrom<Url> 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<Self, Self::Error> {
fn try_from(value: &ParsedUrl) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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),
},

View file

@ -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 }

View file

@ -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<Self, RequirementsTxtParserError> {
// 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<RequirementEntry> for UnresolvedRequirementSpecification {
type Error = Box<ParsedUrlError>;
fn try_from(value: RequirementEntry) -> Result<Self, Self::Error> {
Ok(Self {
impl From<RequirementEntry> 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<RequirementEntry>,
/// Constraints included with `-c`.
pub constraints: Vec<pep508_rs::Requirement>,
pub constraints: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
/// Editables with `-e`.
pub editables: Vec<EditableRequirement>,
/// 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 {
.map_err(|err| RequirementsTxtParserError::Pep508 {
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,
}
}
},
})?;
let hashes = if has_hashes {
@ -1068,17 +1045,17 @@ pub enum RequirementsTxtParserError {
column: usize,
},
UnsupportedRequirement {
source: Pep508Error,
source: Box<Pep508Error<VerbatimParsedUrl>>,
start: usize,
end: usize,
},
Pep508 {
source: Pep508Error,
source: Box<Pep508Error<VerbatimParsedUrl>>,
start: usize,
end: usize,
},
ParsedUrl {
source: Box<ParsedUrlError>,
source: Box<Pep508Error<VerbatimParsedUrl>>,
start: usize,
end: usize,
},

View file

@ -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<VerbatimParsedUrl>),
/// A PEP 508-like, direct URL dependency specifier.
Unnamed(UnnamedRequirement),
Unnamed(UnnamedRequirement<VerbatimParsedUrl>),
}
impl RequirementsTxtRequirement {
@ -31,20 +29,12 @@ impl RequirementsTxtRequirement {
}
}
#[derive(Debug, Error)]
pub enum RequirementsTxtRequirementError {
#[error(transparent)]
ParsedUrl(#[from] Box<ParsedUrlError>),
#[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<Path>,
) -> Result<Self, RequirementsTxtRequirementError> {
) -> Result<Self, Box<Pep508Error<VerbatimParsedUrl>>> {
// 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)
}
}

View file

@ -35,7 +35,28 @@ RequirementsTxt {
],
version_or_url: Some(
Url(
VerbatimUrl {
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,
@ -55,6 +76,7 @@ RequirementsTxt {
"https://github.com/pandas-dev/pandas",
),
},
},
),
),
marker: None,

View file

@ -7,7 +7,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
url: VerbatimParsedUrl {
parsed_url: Path(
ParsedPathUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
path: "<REQUIREMENTS_DIR>/./scripts/packages/black_editable",
editable: false,
},
),
verbatim: VerbatimUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
@ -23,6 +41,7 @@ RequirementsTxt {
"./scripts/packages/black_editable",
),
},
},
extras: [],
marker: None,
origin: Some(
@ -37,7 +56,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
url: VerbatimParsedUrl {
parsed_url: Path(
ParsedPathUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
path: "<REQUIREMENTS_DIR>/./scripts/packages/black_editable",
editable: false,
},
),
verbatim: VerbatimUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
@ -53,6 +90,7 @@ RequirementsTxt {
"./scripts/packages/black_editable",
),
},
},
extras: [
ExtraName(
"dev",
@ -71,7 +109,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
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,
@ -87,6 +143,7 @@ RequirementsTxt {
"file:///scripts/packages/black_editable",
),
},
},
extras: [],
marker: None,
origin: Some(

View file

@ -35,7 +35,28 @@ RequirementsTxt {
],
version_or_url: Some(
Url(
VerbatimUrl {
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,
@ -55,6 +76,7 @@ RequirementsTxt {
"https://github.com/pandas-dev/pandas",
),
},
},
),
),
marker: None,

View file

@ -7,7 +7,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
url: VerbatimParsedUrl {
parsed_url: Path(
ParsedPathUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
path: "<REQUIREMENTS_DIR>/./scripts/packages/black_editable",
editable: false,
},
),
verbatim: VerbatimUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
@ -23,6 +41,7 @@ RequirementsTxt {
"./scripts/packages/black_editable",
),
},
},
extras: [],
marker: None,
origin: Some(
@ -37,7 +56,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
url: VerbatimParsedUrl {
parsed_url: Path(
ParsedPathUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
path: "<REQUIREMENTS_DIR>/./scripts/packages/black_editable",
editable: false,
},
),
verbatim: VerbatimUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
@ -53,6 +90,7 @@ RequirementsTxt {
"./scripts/packages/black_editable",
),
},
},
extras: [
ExtraName(
"dev",
@ -71,7 +109,25 @@ RequirementsTxt {
RequirementEntry {
requirement: Unnamed(
UnnamedRequirement {
url: VerbatimUrl {
url: VerbatimParsedUrl {
parsed_url: Path(
ParsedPathUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false,
},
),
verbatim: VerbatimUrl {
url: Url {
scheme: "file",
cannot_be_a_base: false,
@ -87,6 +143,7 @@ RequirementsTxt {
"file:///scripts/packages/black_editable",
),
},
},
extras: [],
marker: None,
origin: Some(

View file

@ -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 }

View file

@ -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<Regex> =
static DEFAULT_BACKEND: Lazy<Pep517Backend> = 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<ParsedUrlError>),
}
#[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<pep508_rs::Requirement>,
pub requires: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
/// A string naming a Python object that will be used to perform the build.
pub build_backend: Option<String>,
/// 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::<Result<_, _>>()
.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<pep508_rs::Requirement> = serde_json::from_slice::<Vec<pep508_rs::Requirement>>(&contents).map_err(|err| {
let extra_requires: Vec<pep508_rs::Requirement<VerbatimParsedUrl>> = serde_json::from_slice::<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>(&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::<Result<_, _>>()
.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

View file

@ -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 }

View file

@ -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;

View file

@ -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<ParsedUrlError>),
#[error(transparent)]
Reqwest(#[from] BetterReqwestError),
#[error(transparent)]
Client(#[from] uv_client::Error),

View file

@ -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};

View file

@ -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 {

View file

@ -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<VerbatimParsedUrl>,
},
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<VerbatimParsedUrl>,
},
DuplicatePackage {
/// The package that has multiple installed distributions.

View file

@ -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<distribution_types::ParsedUrlError>),
}
/// 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::<Result<_, _>>()?
.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::<Result<_, _>>()?
requires_dist.into_iter().map(Requirement::from).collect()
}
};

View file

@ -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<Pep508Error<VerbatimParsedUrl>>),
#[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<Pep508Error<VerbatimParsedUrl>> for Pep621Error {
fn from(error: Pep508Error<VerbatimParsedUrl>) -> 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<ParsedUrlError>),
#[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<VerbatimParsedUrl>,
project_name: &PackageName,
project_dir: &Path,
project_sources: &BTreeMap<PackageName, Source>,
@ -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() {

View file

@ -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::<Result<_, _>>()?)
.map(Requirement::from)
.collect())
}
/// Infer the package name for a given "unnamed" requirement.
async fn resolve_source_tree(&self, path: &Path) -> Result<Vec<pep508_rs::Requirement>> {
async fn resolve_source_tree(
&self,
path: &Path,
) -> Result<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>> {
// Convert to a buildable source.
let source_tree = fs_err::canonicalize(path).with_context(|| {
format!(

View file

@ -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::<Result<_, _>>()?,
.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,

View file

@ -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::<FuturesOrdered<_>>()
@ -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<VerbatimParsedUrl>,
hasher: &HashStrategy,
index: &InMemoryIndex,
database: &DistributionDatabase<'a, Context>,
) -> Result<pep508_rs::Requirement> {
) -> Result<pep508_rs::Requirement<VerbatimParsedUrl>> {
// 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,23 +126,17 @@ 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.
if path.is_dir() {
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(path.join("PKG-INFO"))
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 = path.display(),
path = parsed_path_url.path.display(),
name = metadata.name
);
return Ok(pep508_rs::Requirement {
@ -154,7 +149,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
}
// Attempt to read a `pyproject.toml` file.
let project_path = path.join("pyproject.toml");
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::<PyProjectToml>(&contents).ok())
@ -163,7 +158,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
if let Some(project) = pyproject.project {
debug!(
"Found PEP 621 metadata for {path} in `pyproject.toml` ({name})",
path = path.display(),
path = parsed_path_url.path.display(),
name = project.name
);
return Ok(pep508_rs::Requirement {
@ -181,7 +176,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
if let Some(name) = poetry.name {
debug!(
"Found Poetry metadata for {path} in `pyproject.toml` ({name})",
path = path.display(),
path = parsed_path_url.path.display(),
name = name
);
return Ok(pep508_rs::Requirement {
@ -197,7 +192,8 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
}
// Attempt to read a `setup.cfg` from the directory.
if let Some(setup_cfg) = fs_err::read_to_string(path.join("setup.cfg"))
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();
@ -210,7 +206,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
if let Ok(name) = PackageName::from_str(name) {
debug!(
"Found setuptools metadata for {path} in `setup.cfg` ({name})",
path = path.display(),
path = parsed_path_url.path.display(),
name = name
);
return Ok(pep508_rs::Requirement {
@ -226,33 +222,23 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> {
}
SourceUrl::Directory(DirectorySourceUrl {
url: &requirement.url,
path: Cow::Owned(path),
})
} else {
SourceUrl::Path(PathSourceUrl {
url: &requirement.url,
path: Cow::Owned(path),
url: &requirement.url.verbatim,
path: Cow::Borrowed(&parsed_path_url.path),
})
}
}
Some(Scheme::Http | Scheme::Https) => SourceUrl::Direct(DirectSourceUrl {
url: &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(),
}),
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,
})
}
_ => {
return Err(anyhow::anyhow!(
"Unsupported scheme for unnamed requirement: {}",
requirement.url
));
}
};
// Fetch the metadata for the distribution.

View file

@ -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<ParsedUrlError>),
/// Something unexpected happened.
#[error("{0}")]
Failure(String),

View file

@ -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;

View file

@ -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<VerbatimParsedUrl>),
#[error(transparent)]
Hash(#[from] HashError),
#[error(transparent)]
ParsedUrl(#[from] Box<ParsedUrlError>),
}
/// A pinned requirement, as extracted from a `requirements.txt` file.
@ -33,9 +31,7 @@ impl Preference {
pub fn from_entry(entry: RequirementEntry) -> Result<Self, PreferenceError> {
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));
}

View file

@ -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)]

View file

@ -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;

View file

@ -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;

View file

@ -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::<anyhow::Result<_, _>>()?;
.map(Requirement::from)
.collect();
for req in manifest.apply(requirements.iter()) {
let Some(ref marker_tree) = req.marker else {
continue;

View file

@ -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};

View file

@ -1063,8 +1063,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.requires_dist
.iter()
.cloned()
.map(Requirement::from_pep508)
.collect::<Result<_, _>>()?;
.map(Requirement::from)
.collect();
let dependencies = PubGrubDependencies::from_requirements(
&requirements,
&self.constraints,
@ -1170,8 +1170,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.requires_dist
.iter()
.cloned()
.map(Requirement::from_pep508)
.collect::<Result<_, _>>()?;
.map(Requirement::from)
.collect();
let dependencies = PubGrubDependencies::from_requirements(
&requirements,
&self.constraints,

View file

@ -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;

View file

@ -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();

View file

@ -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)
}
};

View file

@ -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"] }

View file

@ -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::<Result<_, _>>()?,
.map(Requirement::from)
.collect(),
optional_dependencies: IndexMap::default(),
};
Ok::<_, Box<ParsedUrlError>>(BuiltEditableMetadata {
BuiltEditableMetadata {
built: built_editable.editable,
metadata: built_editable.metadata,
requirements,
}
})
})
.collect::<Result<_, _>>()?;
.collect();
// Validate that the editables are compatible with the target Python version.
for editable in &editables {

View file

@ -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;

View file

@ -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<InstalledPackages: InstalledPackagesProvider>(
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<distribution_types::ParsedUrlError>),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("Installed distribution has unsupported type")]
UnsupportedInstalledDist(#[source] Box<distribution_types::ParsedUrlError>),
UnsupportedInstalledDist(#[source] Box<ParsedUrlError>),
}

View file

@ -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<Requirement>, Vec<UnnamedRequirement>) = spec
let (named, unnamed): (Vec<Requirement>, Vec<UnnamedRequirement<VerbatimParsedUrl>>) = 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::<Vec<_>>();
urls.sort_unstable();
urls.dedup();

View file

@ -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.

View file

@ -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<Vec<BuiltEditableMetadata>, Box<ParsedUrlError>> {
pub(crate) fn as_metadata(&self) -> Vec<BuiltEditableMetadata> {
self.iter()
.map(|editable| {
let dependencies: Vec<_> = editable
@ -167,16 +167,16 @@ impl ResolvedEditables {
.requires_dist
.iter()
.cloned()
.map(Requirement::from_pep508)
.collect::<Result<_, _>>()?;
Ok::<_, Box<ParsedUrlError>>(BuiltEditableMetadata {
.map(Requirement::from)
.collect();
BuiltEditableMetadata {
built: editable.local().clone(),
metadata: editable.metadata().clone(),
requirements: Requirements {
dependencies,
optional_dependencies: IndexMap::default(),
},
})
}
})
.collect()
}

View file

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"###
);