diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index b70c5f5ed934..6c61e0ff3f8b 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -27,20 +27,12 @@ pub struct VerbatimUrl { #[derivative(Ord = "ignore")] #[derivative(Hash = "ignore")] given: Option, - /// Whether the given URL was a relative path. - #[derivative(PartialEq = "ignore")] - #[derivative(Hash = "ignore")] - relative: Option, } impl VerbatimUrl { /// Create a [`VerbatimUrl`] from a [`Url`]. pub fn from_url(url: Url) -> Self { - Self { - url, - given: None, - relative: None, - } + Self { url, given: None } } /// Create a [`VerbatimUrl`] from a file path. @@ -64,21 +56,13 @@ impl VerbatimUrl { url.set_fragment(Some(fragment)); } - Self { - url, - given: None, - relative: None, - } + Self { url, given: None } } /// Parse a URL from a string, expanding any environment variables. pub fn parse_url(given: impl AsRef) -> Result { let url = Url::parse(given.as_ref())?; - Ok(Self { - url, - given: None, - relative: None, - }) + Ok(Self { url, given: None }) } /// Parse a URL from an absolute or relative path. @@ -86,13 +70,11 @@ impl VerbatimUrl { pub fn parse_path(path: impl AsRef, working_dir: impl AsRef) -> Self { let path = path.as_ref(); - let relative = path.is_relative(); - // Convert the path to an absolute path, if necessary. - let path = if relative { - working_dir.as_ref().join(path) - } else { + let path = if path.is_absolute() { path.to_path_buf() + } else { + working_dir.as_ref().join(path) }; // Normalize the path. @@ -115,11 +97,7 @@ impl VerbatimUrl { url.set_fragment(Some(fragment)); } - Self { - url, - given: None, - relative: Some(relative), - } + Self { url, given: None } } /// Parse a URL from an absolute path. @@ -150,11 +128,7 @@ impl VerbatimUrl { url.set_fragment(Some(fragment)); } - Ok(Self { - url, - given: None, - relative: Some(false), - }) + Ok(Self { url, given: None }) } /// Set the verbatim representation of the URL. @@ -186,11 +160,7 @@ impl VerbatimUrl { /// This method should be used sparingly (ideally, not at all), as it represents a loss of the /// verbatim representation. pub fn unknown(url: Url) -> Self { - Self { - url, - given: None, - relative: None, - } + Self { given: None, url } } } diff --git a/crates/uv-resolver/src/resolution/display.rs b/crates/uv-resolver/src/resolution/display.rs index c9bc759d31d1..e2e6a72a4410 100644 --- a/crates/uv-resolver/src/resolution/display.rs +++ b/crates/uv-resolver/src/resolution/display.rs @@ -1,14 +1,11 @@ use std::borrow::Cow; use std::collections::BTreeSet; -use itertools::Itertools; use owo_colors::OwoColorize; use petgraph::visit::EdgeRef; use petgraph::Direction; -use distribution_types::{ - DistributionMetadata, IndexUrl, LocalEditable, Name, SourceAnnotations, Verbatim, -}; +use distribution_types::{IndexUrl, LocalEditable, Name, SourceAnnotations, Verbatim}; use pypi_types::HashDigest; use uv_normalize::PackageName; @@ -155,13 +152,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> { let mut line = match node { Node::Editable(editable) => format!("-e {}", editable.verbatim()), Node::Distribution(dist) => { - let dist = if self.include_extras { - Cow::Borrowed(dist) - } else { - dist.without_extras() - }; - dist.to_requirements_txt().to_string() - + dist.to_requirements_txt(self.include_extras).to_string() } }; diff --git a/crates/uv-resolver/src/resolution/mod.rs b/crates/uv-resolver/src/resolution/mod.rs index 39a261791e3e..ebefefcebd70 100644 --- a/crates/uv-resolver/src/resolution/mod.rs +++ b/crates/uv-resolver/src/resolution/mod.rs @@ -5,6 +5,7 @@ use std::path::Path; use itertools::Itertools; use distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef}; +use pep508_rs::{split_scheme, Scheme}; use pypi_types::{HashDigest, Metadata23}; use uv_normalize::{ExtraName, PackageName}; @@ -32,19 +33,52 @@ impl AnnotatedDist { /// This typically results in a PEP 508 representation of the requirement, but will write an /// unnamed requirement for relative paths, which can't be represented with PEP 508 (but are /// supported in `requirements.txt`). - pub(crate) fn to_requirements_txt(&self) -> Cow { - // If the URL is not _definitively_ an absolute `file://` URL, write it as a relative - // path. + pub(crate) fn to_requirements_txt(&self, include_extras: bool) -> Cow { + // If the URL is not _definitively_ an absolute `file://` URL, write it as a relative path. if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() { let given = url.verbatim(); - if !given.strip_prefix("file://").is_some_and(|path| { - path.starts_with("${PROJECT_ROOT}") || Path::new(path).is_absolute() - }) { - return given; + match split_scheme(&given) { + Some((scheme, path)) => { + match Scheme::parse(scheme) { + Some(Scheme::File) => { + if path + .strip_prefix("//localhost") + .filter(|path| path.starts_with('/')) + .is_some() + { + // Always absolute; nothing to do. + } else if let Some(path) = path.strip_prefix("//") { + // Strip the prefix, to convert, e.g., `file://flask-3.0.3-py3-none-any.whl` to `flask-3.0.3-py3-none-any.whl`. + // + // However, we should allow any of the following: + // - `file://flask-3.0.3-py3-none-any.whl` + // - `file://C:\Users\user\flask-3.0.3-py3-none-any.whl` + // - `file:///C:\Users\user\flask-3.0.3-py3-none-any.whl` + if !path.starts_with("${PROJECT_ROOT}") + && !Path::new(path).has_root() + { + return Cow::Owned(path.to_string()); + } + } else { + // Ex) `file:./flask-3.0.3-py3-none-any.whl` + return given; + } + } + Some(_) => {} + None => { + // Ex) `flask @ C:\Users\user\flask-3.0.3-py3-none-any.whl` + return given; + } + } + } + None => { + // Ex) `flask @ flask-3.0.3-py3-none-any.whl` + return given; + } } } - if self.extras.is_empty() { + if self.extras.is_empty() || !include_extras { self.dist.verbatim() } else { let mut extras = self.extras.clone(); @@ -58,20 +92,6 @@ impl AnnotatedDist { )) } } - - /// Return the [`AnnotatedDist`] without any extras. - pub(crate) fn without_extras(&self) -> Cow { - if self.extras.is_empty() { - Cow::Borrowed(self) - } else { - Cow::Owned(AnnotatedDist { - dist: self.dist.clone(), - extras: Vec::new(), - hashes: self.hashes.clone(), - metadata: self.metadata.clone(), - }) - } - } } impl Name for AnnotatedDist { diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 744a02d88c21..f4ceb28d31d7 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -2065,7 +2065,7 @@ fn allowed_transitive_url_path_dependency() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - hatchling-editable @ ${HATCH_PATH} + ${HATCH_PATH} # via -r requirements.in iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4 # via hatchling-editable @@ -2510,7 +2510,7 @@ fn compile_wheel_path_dependency() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ file:flask-3.0.0-py3-none-any.whl + file:flask-3.0.0-py3-none-any.whl # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -2543,7 +2543,7 @@ fn compile_wheel_path_dependency() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ file://flask-3.0.0-py3-none-any.whl + flask-3.0.0-py3-none-any.whl # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -2576,7 +2576,7 @@ fn compile_wheel_path_dependency() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ ./flask-3.0.0-py3-none-any.whl + ./flask-3.0.0-py3-none-any.whl # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -2609,7 +2609,7 @@ fn compile_wheel_path_dependency() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ [TEMP_DIR]/flask-3.0.0-py3-none-any.whl + [TEMP_DIR]/flask-3.0.0-py3-none-any.whl # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -3192,7 +3192,7 @@ fn respect_http_env_var() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ ${URL} + ${URL} # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -3233,7 +3233,7 @@ fn respect_unnamed_env_var() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ ${URL} + ${URL} # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -3305,7 +3305,7 @@ fn respect_file_env_var() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ ${FILE_PATH} + ${FILE_PATH} # via -r requirements.in itsdangerous==2.1.2 # via flask @@ -3468,7 +3468,7 @@ fn recursive_extras_direct_url() -> Result<()> { # via aiohttp attrs==23.2.0 # via aiohttp - black @ ../../scripts/packages/black_editable + ../../scripts/packages/black_editable # via -r [TEMP_DIR]/requirements.in frozenlist==1.4.1 # via @@ -3964,7 +3964,7 @@ fn generate_hashes_local_directory() -> Result<()> { --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f # via anyio - poetry-editable @ ../../scripts/packages/poetry_editable + ../../scripts/packages/poetry_editable # via -r [TEMP_DIR]/requirements.in sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ @@ -6799,11 +6799,11 @@ fn compile_root_uri_non_editable() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - black @ ${BLACK_PATH} + ${BLACK_PATH} # via # -r requirements.in # root-editable - root-editable @ ${ROOT_PATH} + ${ROOT_PATH} # via -r requirements.in ----- stderr ----- @@ -7186,7 +7186,7 @@ fn unnamed_path_requirement() -> Result<()> { # via # httpx # poetry-editable - black @ ../../scripts/packages/black_editable + ../../scripts/packages/black_editable # via -r [TEMP_DIR]/requirements.in certifi==2024.2.2 # via @@ -7206,13 +7206,13 @@ fn unnamed_path_requirement() -> Result<()> { # anyio # httpx # requests - poetry-editable @ ../../scripts/packages/poetry_editable + ../../scripts/packages/poetry_editable # via -r [TEMP_DIR]/requirements.in requests==2.31.0 # via setup-cfg-editable - setup-cfg-editable @ ../../scripts/packages/setup_cfg_editable + ../../scripts/packages/setup_cfg_editable # via -r [TEMP_DIR]/requirements.in - setup-py-editable @ ../../scripts/packages/setup_py_editable + ../../scripts/packages/setup_py_editable # via -r [TEMP_DIR]/requirements.in sniffio==1.3.1 # via @@ -7322,7 +7322,7 @@ fn dynamic_dependencies() -> Result<()> { # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in anyio==4.3.0 # via hatchling-dynamic - hatchling-dynamic @ ../../scripts/packages/hatchling_dynamic + ../../scripts/packages/hatchling_dynamic # via -r [TEMP_DIR]/requirements.in idna==3.6 # via anyio @@ -7698,7 +7698,7 @@ requires-python = ">3.8" # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in anyio @ file://[TEMP_DIR]/anyio/ # via lib - example @ ./app + ./app # via -r requirements.in idna==3.6 # via anyio @@ -7786,7 +7786,7 @@ requires-python = ">3.8" # via # --override overrides.txt # lib - example @ ./app + ./app # via -r requirements.in idna==3.6 # via anyio @@ -7896,12 +7896,12 @@ requires-python = ">3.8" ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt --constraint constraints.txt - anyio @ ./anyio + ./anyio # via # -c constraints.txt # --override overrides.txt # lib - example @ ./app + ./app # via -r requirements.in idna==3.6 # via anyio @@ -7947,7 +7947,7 @@ requires-python = ">3.8" # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in click==8.1.7 # via flask - example @ . + . # via -r requirements.in flask==2.0.0rc1 # via example