Skip to content

Commit

Permalink
Write relative paths with unnamed requirement syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed May 21, 2024
1 parent 776a7e4 commit 733590c
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 23 deletions.
48 changes: 39 additions & 9 deletions crates/pep508-rs/src/verbatim_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@ pub struct VerbatimUrl {
#[derivative(Ord = "ignore")]
#[derivative(Hash = "ignore")]
given: Option<String>,
/// Whether the given URL was a relative path.
#[derivative(PartialEq = "ignore")]
#[derivative(Hash = "ignore")]
relative: Option<bool>,
}

impl VerbatimUrl {
/// Create a [`VerbatimUrl`] from a [`Url`].
pub fn from_url(url: Url) -> Self {
Self { url, given: None }
Self {
url,
given: None,
relative: None,
}
}

/// Create a [`VerbatimUrl`] from a file path.
Expand All @@ -56,25 +64,35 @@ impl VerbatimUrl {
url.set_fragment(Some(fragment));
}

Self { url, given: None }
Self {
url,
given: None,
relative: None,
}
}

/// Parse a URL from a string, expanding any environment variables.
pub fn parse_url(given: impl AsRef<str>) -> Result<Self, ParseError> {
let url = Url::parse(given.as_ref())?;
Ok(Self { url, given: None })
Ok(Self {
url,
given: None,
relative: None,
})
}

/// Parse a URL from an absolute or relative path.
#[cfg(feature = "non-pep508-extensions")] // PEP 508 arguably only allows absolute file URLs.
pub fn parse_path(path: impl AsRef<Path>, working_dir: impl AsRef<Path>) -> Self {
let path = path.as_ref();

let relative = path.is_relative();

// Convert the path to an absolute path, if necessary.
let path = if path.is_absolute() {
path.to_path_buf()
} else {
let path = if relative {
working_dir.as_ref().join(path)
} else {
path.to_path_buf()
};

// Normalize the path.
Expand All @@ -97,7 +115,11 @@ impl VerbatimUrl {
url.set_fragment(Some(fragment));
}

Self { url, given: None }
Self {
url,
given: None,
relative: Some(relative),
}
}

/// Parse a URL from an absolute path.
Expand Down Expand Up @@ -128,7 +150,11 @@ impl VerbatimUrl {
url.set_fragment(Some(fragment));
}

Ok(Self { url, given: None })
Ok(Self {
url,
given: None,
relative: Some(false),
})
}

/// Set the verbatim representation of the URL.
Expand Down Expand Up @@ -160,7 +186,11 @@ 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 { given: None, url }
Self {
url,
given: None,
relative: None,
}
}
}

Expand Down
18 changes: 6 additions & 12 deletions crates/uv-resolver/src/resolution/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,13 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
let mut line = match node {
Node::Editable(editable) => format!("-e {}", editable.verbatim()),
Node::Distribution(dist) => {
if self.include_extras && !dist.extras.is_empty() {
let mut extras = dist.extras.clone();
extras.sort_unstable();
extras.dedup();
format!(
"{}[{}]{}",
dist.name(),
extras.into_iter().join(", "),
dist.version_or_url().verbatim()
)
let dist = if self.include_extras {
Cow::Borrowed(dist)
} else {
dist.verbatim().to_string()
}
dist.without_extras()
};
dist.to_requirements_txt().to_string()

}
};

Expand Down
57 changes: 55 additions & 2 deletions crates/uv-resolver/src/resolution/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::borrow::Cow;
use std::fmt::Display;
use std::path::Path;

use distribution_types::{DistributionMetadata, Name, ResolvedDist, VersionOrUrlRef};
use itertools::Itertools;

use distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef};
use pypi_types::{HashDigest, Metadata23};
use uv_normalize::{ExtraName, PackageName};

Expand All @@ -13,14 +17,63 @@ mod graph;
/// A pinned package with its resolved distribution and metadata. The [`ResolvedDist`] refers to a
/// specific distribution (e.g., a specific wheel), while the [`Metadata23`] refers to the metadata
/// for the package-version pair.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct AnnotatedDist {
pub(crate) dist: ResolvedDist,
pub(crate) extras: Vec<ExtraName>,
pub(crate) hashes: Vec<HashDigest>,
pub(crate) metadata: Metadata23,
}

impl AnnotatedDist {
/// Convert the [`AnnotatedDist`] to a requirement that adheres to the `requirements.txt`
/// format.
///
/// 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<str> {
// 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;
}
}

if self.extras.is_empty() {
self.dist.verbatim()
} else {
let mut extras = self.extras.clone();
extras.sort_unstable();
extras.dedup();
Cow::Owned(format!(
"{}[{}]{}",
self.name(),
extras.into_iter().join(", "),
self.version_or_url().verbatim()
))
}
}

/// Return the [`AnnotatedDist`] without any extras.
pub(crate) fn without_extras(&self) -> Cow<AnnotatedDist> {
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 {
fn name(&self) -> &PackageName {
self.dist.name()
Expand Down

0 comments on commit 733590c

Please sign in to comment.