Support locking relative paths (#4205)

By splitting `path` into a lockable, relative (or absolute) and an
absolute installable path and by splitting between urls and paths by
dist type, we can store relative paths in the lockfile.
This commit is contained in:
konsti 2024-06-11 13:58:03 +02:00 committed by GitHub
parent 33cf47182f
commit 44833801b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 533 additions and 364 deletions

View file

@ -2,7 +2,9 @@
// as we build out universal locking.
#![allow(dead_code, unreachable_code, unused_variables)]
use std::borrow::Cow;
use std::collections::{BTreeMap, VecDeque};
use std::fmt::{Debug, Display};
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -11,6 +13,7 @@ use either::Either;
use indexmap::IndexMap;
use petgraph::visit::EdgeRef;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Deserializer};
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
use url::Url;
@ -22,7 +25,7 @@ use distribution_types::{
RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError,
};
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl};
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError};
use platform_tags::{TagCompatibility, TagPriority, Tags};
use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl};
use uv_configuration::ExtrasSpecification;
@ -145,6 +148,7 @@ impl Lock {
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution(
&self,
workspace_root: &Path,
marker_env: &MarkerEnvironment,
tags: &Tags,
root_name: &PackageName,
@ -202,7 +206,7 @@ impl Lock {
}
}
let name = dist.id.name.clone();
let resolved_dist = ResolvedDist::Installable(dist.to_dist(tags)?);
let resolved_dist = ResolvedDist::Installable(dist.to_dist(workspace_root, tags)?);
map.insert(name, resolved_dist);
}
let diagnostics = vec![];
@ -485,9 +489,9 @@ impl TryFrom<LockWire> for Lock {
// Also check that our sources are consistent with whether we have
// hashes or not.
let requires_hash = dist.id.source.kind.requires_hash();
let requires_hash = dist.id.source.requires_hash();
if let Some(ref sdist) = dist.sdist {
if requires_hash != sdist.hash.is_some() {
if requires_hash != sdist.hash().is_some() {
return Err(LockErrorKind::Hash {
id: dist.id.clone(),
artifact_type: "source distribution",
@ -581,14 +585,14 @@ impl Distribution {
}
/// Convert the [`Distribution`] to a [`Dist`] that can be used in installation.
fn to_dist(&self, tags: &Tags) -> Result<Dist, LockError> {
fn to_dist(&self, workspace_root: &Path, tags: &Tags) -> Result<Dist, LockError> {
if let Some(best_wheel_index) = self.find_best_wheel(tags) {
return match &self.id.source.kind {
SourceKind::Registry => {
return match &self.id.source {
Source::Registry(url) => {
let wheels = self
.wheels
.iter()
.map(|wheel| wheel.to_registry_dist(&self.id.source))
.map(|wheel| wheel.to_registry_dist(url, &self.id.source))
.collect();
let reg_built_dist = RegistryBuiltDist {
wheels,
@ -597,41 +601,46 @@ impl Distribution {
};
Ok(Dist::Built(BuiltDist::Registry(reg_built_dist)))
}
SourceKind::Path => {
Source::Path(path) => {
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
let path_dist = PathBuiltDist {
filename,
url: VerbatimUrl::from_url(self.id.source.url.clone()),
path: self.id.source.url.to_file_path().unwrap(),
url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| {
LockErrorKind::VerbatimUrl {
id: self.id.clone(),
err,
}
})?,
path: path.clone(),
};
let built_dist = BuiltDist::Path(path_dist);
Ok(Dist::Built(built_dist))
}
SourceKind::Direct(direct) => {
Source::Direct(url, direct) => {
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
let url = Url::from(ParsedArchiveUrl {
url: self.id.source.url.clone(),
url: url.clone(),
subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
});
let direct_dist = DirectUrlBuiltDist {
filename,
location: self.id.source.url.clone(),
location: url.clone(),
url: VerbatimUrl::from_url(url),
};
let built_dist = BuiltDist::DirectUrl(direct_dist);
Ok(Dist::Built(built_dist))
}
SourceKind::Git(_) => Err(LockErrorKind::InvalidWheelSource {
Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource {
id: self.id.clone(),
source_type: "Git",
}
.into()),
SourceKind::Directory => Err(LockErrorKind::InvalidWheelSource {
Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource {
id: self.id.clone(),
source_type: "directory",
}
.into()),
SourceKind::Editable => Err(LockErrorKind::InvalidWheelSource {
Source::Editable(_) => Err(LockErrorKind::InvalidWheelSource {
id: self.id.clone(),
source_type: "editable",
}
@ -640,43 +649,59 @@ impl Distribution {
}
if let Some(sdist) = &self.sdist {
return match &self.id.source.kind {
SourceKind::Path => {
return match &self.id.source {
Source::Path(path) => {
let path_dist = PathSourceDist {
name: self.id.name.clone(),
url: VerbatimUrl::from_url(self.id.source.url.clone()),
path: self.id.source.url.to_file_path().unwrap(),
url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| {
LockErrorKind::VerbatimUrl {
id: self.id.clone(),
err,
}
})?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
};
let source_dist = distribution_types::SourceDist::Path(path_dist);
Ok(Dist::Source(source_dist))
}
SourceKind::Directory => {
Source::Directory(path) => {
let dir_dist = DirectorySourceDist {
name: self.id.name.clone(),
url: VerbatimUrl::from_url(self.id.source.url.clone()),
path: self.id.source.url.to_file_path().unwrap(),
url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| {
LockErrorKind::VerbatimUrl {
id: self.id.clone(),
err,
}
})?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
editable: false,
};
let source_dist = distribution_types::SourceDist::Directory(dir_dist);
Ok(Dist::Source(source_dist))
}
SourceKind::Editable => {
Source::Editable(path) => {
let dir_dist = DirectorySourceDist {
name: self.id.name.clone(),
url: VerbatimUrl::from_url(self.id.source.url.clone()),
path: self.id.source.url.to_file_path().unwrap(),
url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| {
LockErrorKind::VerbatimUrl {
id: self.id.clone(),
err,
}
})?,
install_path: workspace_root.join(path),
lock_path: path.clone(),
editable: true,
};
let source_dist = distribution_types::SourceDist::Directory(dir_dist);
Ok(Dist::Source(source_dist))
}
SourceKind::Git(git) => {
Source::Git(url, git) => {
// Reconstruct the `GitUrl` from the `GitSource`.
let git_url = uv_git::GitUrl::new(
self.id.source.url.clone(),
GitReference::from(git.kind.clone()),
)
.with_precise(git.precise);
let git_url =
uv_git::GitUrl::new(url.clone(), GitReference::from(git.kind.clone()))
.with_precise(git.precise);
// Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl {
@ -693,32 +718,32 @@ impl Distribution {
let source_dist = distribution_types::SourceDist::Git(git_dist);
Ok(Dist::Source(source_dist))
}
SourceKind::Direct(direct) => {
Source::Direct(url, direct) => {
let url = Url::from(ParsedArchiveUrl {
url: self.id.source.url.clone(),
url: url.clone(),
subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
});
let direct_dist = DirectUrlSourceDist {
name: self.id.name.clone(),
location: self.id.source.url.clone(),
location: url.clone(),
subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
url: VerbatimUrl::from_url(url),
};
let source_dist = distribution_types::SourceDist::DirectUrl(direct_dist);
Ok(Dist::Source(source_dist))
}
SourceKind::Registry => {
Source::Registry(url) => {
let file = Box::new(distribution_types::File {
dist_info_metadata: false,
filename: sdist.url.filename().unwrap().to_string(),
filename: sdist.filename().unwrap().to_string(),
hashes: vec![],
requires_python: None,
size: sdist.size,
size: sdist.size(),
upload_time_utc_ms: None,
url: FileLocation::AbsoluteUrl(sdist.url.to_string()),
url: FileLocation::AbsoluteUrl(url.to_string()),
yanked: None,
});
let index = IndexUrl::Url(VerbatimUrl::from_url(self.id.source.url.clone()));
let index = IndexUrl::Url(VerbatimUrl::from_url(url.clone()));
let reg_dist = RegistrySourceDist {
name: self.id.name.clone(),
version: self.id.version.clone(),
@ -765,10 +790,10 @@ impl Distribution {
/// Returns the [`ResolvedRepositoryReference`] for the distribution, if it is a Git source.
pub fn as_git_ref(&self) -> Option<ResolvedRepositoryReference> {
match &self.id.source.kind {
SourceKind::Git(git) => Some(ResolvedRepositoryReference {
match &self.id.source {
Source::Git(url, git) => Some(ResolvedRepositoryReference {
reference: RepositoryReference {
url: RepositoryUrl::new(&self.id.source.url),
url: RepositoryUrl::new(url),
reference: GitReference::from(git.kind.clone()),
},
sha: git.precise,
@ -804,10 +829,28 @@ impl std::fmt::Display for DistributionId {
}
}
/// NOTE: Care should be taken when adding variants to this enum. Namely, new
/// variants should be added without changing the relative ordering of other
/// variants. Otherwise, this could cause the lock file to have a different
/// canonical ordering of distributions.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
struct Source {
kind: SourceKind,
url: Url,
enum Source {
Registry(Url),
Git(Url, GitSource),
Direct(Url, DirectSource),
Path(PathBuf),
Directory(PathBuf),
Editable(PathBuf),
}
/// A [`PathBuf`], but we show `.` instead of an empty path.
fn serialize_path_with_dot(path: &Path) -> Cow<str> {
let path = path.to_string_lossy();
if path.is_empty() {
Cow::Borrowed(".")
} else {
path
}
}
impl Source {
@ -862,70 +905,58 @@ impl Source {
}
fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Source {
Source {
kind: SourceKind::Direct(DirectSource { subdirectory: None }),
url: direct_dist.url.to_url(),
}
Source::Direct(
direct_dist.url.to_url(),
DirectSource { subdirectory: None },
)
}
fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Source {
Source {
kind: SourceKind::Direct(DirectSource {
Source::Direct(
direct_dist.url.to_url(),
DirectSource {
subdirectory: direct_dist
.subdirectory
.as_deref()
.and_then(Path::to_str)
.map(ToString::to_string),
}),
url: direct_dist.url.to_url(),
}
},
)
}
fn from_path_built_dist(path_dist: &PathBuiltDist) -> Source {
Source {
kind: SourceKind::Path,
url: path_dist.url.to_url(),
}
Source::Path(path_dist.path.clone())
}
fn from_path_source_dist(path_dist: &PathSourceDist) -> Source {
Source {
kind: SourceKind::Path,
url: path_dist.url.to_url(),
}
Source::Path(path_dist.install_path.clone())
}
fn from_directory_source_dist(directory_dist: &DirectorySourceDist) -> Source {
Source {
kind: if directory_dist.editable {
SourceKind::Editable
} else {
SourceKind::Directory
},
url: directory_dist.url.to_url(),
if directory_dist.editable {
Source::Editable(directory_dist.lock_path.clone())
} else {
Source::Directory(directory_dist.lock_path.clone())
}
}
fn from_index_url(index_url: &IndexUrl) -> Source {
match *index_url {
IndexUrl::Pypi(ref verbatim_url) => Source {
kind: SourceKind::Registry,
url: verbatim_url.to_url(),
},
IndexUrl::Url(ref verbatim_url) => Source {
kind: SourceKind::Registry,
url: verbatim_url.to_url(),
},
IndexUrl::Path(ref verbatim_url) => Source {
kind: SourceKind::Path,
url: verbatim_url.to_url(),
},
IndexUrl::Pypi(ref verbatim_url) => Source::Registry(verbatim_url.to_url()),
IndexUrl::Url(ref verbatim_url) => Source::Registry(verbatim_url.to_url()),
// TODO(konsti): Retain path on index url without converting to URL.
IndexUrl::Path(ref verbatim_url) => Source::Path(
verbatim_url
.to_file_path()
.expect("Could not convert index url to path"),
),
}
}
fn from_git_dist(git_dist: &GitSourceDist) -> Source {
Source {
kind: SourceKind::Git(GitSource {
Source::Git(
locked_git_url(git_dist),
GitSource {
kind: GitSourceKind::from(git_dist.git.reference().clone()),
precise: git_dist.git.precise().expect("precise commit"),
subdirectory: git_dist
@ -933,9 +964,8 @@ impl Source {
.as_deref()
.and_then(Path::to_str)
.map(ToString::to_string),
}),
url: locked_git_url(git_dist),
}
},
)
}
}
@ -943,45 +973,45 @@ impl std::str::FromStr for Source {
type Err = SourceParseError;
fn from_str(s: &str) -> Result<Source, SourceParseError> {
let (kind, url) = s.split_once('+').ok_or_else(|| SourceParseError::NoPlus {
let (kind, url_or_path) = s.split_once('+').ok_or_else(|| SourceParseError::NoPlus {
given: s.to_string(),
})?;
let mut url = Url::parse(url).map_err(|err| SourceParseError::InvalidUrl {
given: s.to_string(),
err,
})?;
match kind {
"registry" => Ok(Source {
kind: SourceKind::Registry,
url,
}),
"git" => Ok(Source {
kind: SourceKind::Git(GitSource::from_url(&mut url).map_err(|err| match err {
"registry" => {
let url = Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl {
given: s.to_string(),
err,
})?;
Ok(Source::Registry(url))
}
"git" => {
let mut url =
Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl {
given: s.to_string(),
err,
})?;
let git_source = GitSource::from_url(&mut url).map_err(|err| match err {
GitSourceError::InvalidSha => SourceParseError::InvalidSha {
given: s.to_string(),
},
GitSourceError::MissingSha => SourceParseError::MissingSha {
given: s.to_string(),
},
})?),
url,
}),
"direct" => Ok(Source {
kind: SourceKind::Direct(DirectSource::from_url(&mut url)),
url,
}),
"path" => Ok(Source {
kind: SourceKind::Path,
url,
}),
"directory" => Ok(Source {
kind: SourceKind::Directory,
url,
}),
"editable" => Ok(Source {
kind: SourceKind::Editable,
url,
}),
})?;
Ok(Source::Git(url, git_source))
}
"direct" => {
let mut url =
Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl {
given: s.to_string(),
err,
})?;
let direct_source = DirectSource::from_url(&mut url);
Ok(Source::Direct(url, direct_source))
}
"path" => Ok(Source::Path(PathBuf::from(url_or_path))),
"directory" => Ok(Source::Directory(PathBuf::from(url_or_path))),
"editable" => Ok(Source::Editable(PathBuf::from(url_or_path))),
name => Err(SourceParseError::UnrecognizedSourceName {
given: s.to_string(),
name: name.to_string(),
@ -992,7 +1022,14 @@ impl std::str::FromStr for Source {
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}+{}", self.kind.name(), self.url)
match self {
Source::Registry(url) | Source::Git(url, _) | Source::Direct(url, _) => {
write!(f, "{}+{}", self.name(), url)
}
Source::Path(path) | Source::Directory(path) | Source::Editable(path) => {
write!(f, "{}+{}", self.name(), serialize_path_with_dot(path))
}
}
}
}
@ -1006,29 +1043,15 @@ impl<'de> serde::Deserialize<'de> for Source {
}
}
/// NOTE: Care should be taken when adding variants to this enum. Namely, new
/// variants should be added without changing the relative ordering of other
/// variants. Otherwise, this could cause the lock file to have a different
/// canonical ordering of distributions.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
enum SourceKind {
Registry,
Git(GitSource),
Direct(DirectSource),
Path,
Directory,
Editable,
}
impl SourceKind {
impl Source {
fn name(&self) -> &str {
match *self {
SourceKind::Registry => "registry",
SourceKind::Git(_) => "git",
SourceKind::Direct(_) => "direct",
SourceKind::Path => "path",
SourceKind::Directory => "directory",
SourceKind::Editable => "editable",
Self::Registry(..) => "registry",
Self::Git(..) => "git",
Self::Direct(..) => "direct",
Self::Path(..) => "path",
Self::Directory(..) => "directory",
Self::Editable(..) => "editable",
}
}
@ -1038,8 +1061,8 @@ impl SourceKind {
/// _not_ be present.
fn requires_hash(&self) -> bool {
match *self {
SourceKind::Registry | SourceKind::Direct(_) | SourceKind::Path => true,
SourceKind::Git(_) | SourceKind::Directory | SourceKind::Editable => false,
Self::Registry(..) | Self::Direct(..) | Self::Path(..) => true,
Self::Git(..) | Self::Directory(..) | Self::Editable(..) => false,
}
}
}
@ -1127,12 +1150,7 @@ enum GitSourceKind {
/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
#[derive(Clone, Debug, serde::Deserialize)]
struct SourceDist {
/// A URL or file path (via `file://`) where the source dist that was
/// locked against was found. The location does not need to exist in the
/// future, so this should be treated as only a hint to where to look
/// and/or recording where the source dist file originally came from.
url: Url,
struct SourceDistMetadata {
/// A hash of the source distribution.
///
/// This is only present for source distributions that come from registries
@ -1145,15 +1163,79 @@ struct SourceDist {
size: Option<u64>,
}
/// A URL or file path where the source dist that was
/// locked against was found. The location does not need to exist in the
/// future, so this should be treated as only a hint to where to look
/// and/or recording where the source dist file originally came from.
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(untagged)]
enum SourceDist {
Url {
url: Url,
#[serde(flatten)]
metadata: SourceDistMetadata,
},
Path {
#[serde(deserialize_with = "deserialize_path_with_dot")]
path: PathBuf,
#[serde(flatten)]
metadata: SourceDistMetadata,
},
}
/// A [`PathBuf`], but we show `.` instead of an empty path.
fn deserialize_path_with_dot<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
if path == "." {
Ok(PathBuf::new())
} else {
Ok(PathBuf::from(path))
}
}
impl SourceDist {
pub(crate) fn filename(&self) -> Option<Cow<str>> {
match self {
SourceDist::Url { url, .. } => url.filename().ok(),
SourceDist::Path { path, .. } => {
path.file_name().map(|filename| filename.to_string_lossy())
}
}
}
fn hash(&self) -> Option<&Hash> {
match &self {
SourceDist::Url { metadata, .. } => metadata.hash.as_ref(),
SourceDist::Path { metadata, .. } => metadata.hash.as_ref(),
}
}
fn size(&self) -> Option<u64> {
match &self {
SourceDist::Url { metadata, .. } => metadata.size,
SourceDist::Path { metadata, .. } => metadata.size,
}
}
}
impl SourceDist {
/// Returns the TOML representation of this source distribution.
fn to_toml(&self) -> Result<InlineTable> {
let mut table = InlineTable::new();
table.insert("url", Value::from(self.url.to_string()));
if let Some(ref hash) = self.hash {
match &self {
SourceDist::Url { url, .. } => {
table.insert("url", Value::from(url.as_str()));
}
SourceDist::Path { path, .. } => {
table.insert("path", Value::from(serialize_path_with_dot(path).as_ref()));
}
}
if let Some(hash) = self.hash() {
table.insert("hash", Value::from(hash.to_string()));
}
if let Some(size) = self.size {
if let Some(size) = self.size() {
table.insert("size", Value::from(i64::try_from(size)?));
}
Ok(table)
@ -1219,30 +1301,39 @@ impl SourceDist {
.map_err(LockError::from)?;
let hash = reg_dist.file.hashes.first().cloned().map(Hash::from);
let size = reg_dist.file.size;
Ok(SourceDist { url, hash, size })
Ok(SourceDist::Url {
url,
metadata: SourceDistMetadata { hash, size },
})
}
fn from_direct_dist(direct_dist: &DirectUrlSourceDist, hashes: &[HashDigest]) -> SourceDist {
SourceDist {
SourceDist::Url {
url: direct_dist.url.to_url(),
hash: hashes.first().cloned().map(Hash::from),
size: None,
metadata: SourceDistMetadata {
hash: hashes.first().cloned().map(Hash::from),
size: None,
},
}
}
fn from_git_dist(git_dist: &GitSourceDist, hashes: &[HashDigest]) -> SourceDist {
SourceDist {
SourceDist::Url {
url: locked_git_url(git_dist),
hash: hashes.first().cloned().map(Hash::from),
size: None,
metadata: SourceDistMetadata {
hash: hashes.first().cloned().map(Hash::from),
size: None,
},
}
}
fn from_path_dist(path_dist: &PathSourceDist, hashes: &[HashDigest]) -> SourceDist {
SourceDist {
url: path_dist.url.to_url(),
hash: hashes.first().cloned().map(Hash::from),
size: None,
SourceDist::Path {
path: path_dist.lock_path.clone(),
metadata: SourceDistMetadata {
hash: hashes.first().cloned().map(Hash::from),
size: None,
},
}
}
@ -1250,10 +1341,12 @@ impl SourceDist {
directory_dist: &DirectorySourceDist,
hashes: &[HashDigest],
) -> SourceDist {
SourceDist {
url: directory_dist.url.to_url(),
hash: hashes.first().cloned().map(Hash::from),
size: None,
SourceDist::Path {
path: directory_dist.lock_path.clone(),
metadata: SourceDistMetadata {
hash: hashes.first().cloned().map(Hash::from),
size: None,
},
}
}
}
@ -1439,7 +1532,7 @@ impl Wheel {
}
}
fn to_registry_dist(&self, source: &Source) -> RegistryBuiltWheel {
fn to_registry_dist(&self, url: &Url, source: &Source) -> RegistryBuiltWheel {
let filename: WheelFilename = self.filename.clone();
let file = Box::new(distribution_types::File {
dist_info_metadata: false,
@ -1451,7 +1544,7 @@ impl Wheel {
url: FileLocation::AbsoluteUrl(self.url.to_string()),
yanked: None,
});
let index = IndexUrl::Url(VerbatimUrl::from_url(source.url.clone()));
let index = IndexUrl::Url(VerbatimUrl::from_url(url.clone()));
RegistryBuiltWheel {
filename,
file,
@ -1632,7 +1725,7 @@ impl<'de> serde::Deserialize<'de> for Hash {
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct LockError(Box<LockErrorKind>);
@ -1651,7 +1744,7 @@ where
/// For example, if there are two or more duplicative distributions given
/// to `Lock::new`, then an error is returned. It's likely that the fault
/// is with the caller somewhere in such cases.
#[derive(Clone, Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error)]
enum LockErrorKind {
/// An error that occurs when multiple distributions with the same
/// ID were found.
@ -1732,7 +1825,7 @@ enum LockErrorKind {
},
/// An error that occurs when a hash is expected (or not) for a particular
/// artifact, but one was not found (or was).
#[error("since the distribution `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", source = id.source.kind.name(), expected = if *expected { "expected" } else { "not expected" })]
#[error("since the distribution `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", source = id.source.name(), expected = if *expected { "expected" } else { "not expected" })]
Hash {
/// The ID of the distribution that has a missing hash.
id: DistributionId,
@ -1777,6 +1870,15 @@ enum LockErrorKind {
/// The ID of the distribution that has a missing base.
id: DistributionId,
},
/// An error that occurs when converting between URLs and paths.
#[error("found dependency `{id}` with no locked distribution")]
VerbatimUrl {
/// The ID of the distribution that has a missing base.
id: DistributionId,
/// The inner error we forward.
#[source]
err: VerbatimUrlError,
},
}
/// An error that occurs when a source string could not be parsed.
@ -1827,7 +1929,7 @@ impl std::error::Error for HashParseError {}
impl std::fmt::Display for HashParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
Display::fmt(self.0, f)
}
}

View file

@ -334,7 +334,8 @@ impl PubGrubRequirement {
RequirementSource::Path {
editable,
url,
path,
install_path,
lock_path,
} => {
let Some(expected) = urls.get(&requirement.name) else {
return Err(ResolveError::DisallowedUrl(
@ -344,7 +345,8 @@ impl PubGrubRequirement {
};
let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source(
path.clone(),
install_path.clone(),
lock_path.clone(),
*editable,
url.to_url(),
));

View file

@ -177,7 +177,9 @@ fn iter_locals(source: &RequirementSource) -> Box<dyn Iterator<Item = Version> +
.filter(pep440_rs::Version::is_local),
),
RequirementSource::Git { .. } => Box::new(iter::empty()),
RequirementSource::Path { path, .. } => Box::new(
RequirementSource::Path {
install_path: path, ..
} => Box::new(
path.file_name()
.and_then(|filename| {
let filename = filename.to_string_lossy();

View file

@ -53,13 +53,15 @@ impl Urls {
}
}
RequirementSource::Path {
path,
install_path,
lock_path,
editable,
url,
} => {
let url = VerbatimParsedUrl {
parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
path.clone(),
install_path.clone(),
lock_path.clone(),
*editable,
url.to_url(),
)),
@ -140,7 +142,8 @@ impl Urls {
a.subdirectory == b.subdirectory && git.same_ref(&a.url, &b.url)
}
(ParsedUrl::Path(a), ParsedUrl::Path(b)) => {
a.path == b.path || is_same_file(&a.path, &b.path).unwrap_or(false)
a.install_path == b.install_path
|| is_same_file(&a.install_path, &b.install_path).unwrap_or(false)
}
_ => false,
}

View file

@ -12,20 +12,9 @@ Ok(
"anyio",
),
version: "4.3.0",
source: Source {
kind: Path,
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
},
source: Path(
"file:///foo/bar",
),
},
sdist: None,
wheels: [
@ -80,20 +69,9 @@ Ok(
"anyio",
),
version: "4.3.0",
source: Source {
kind: Path,
url: Url {
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
},
source: Path(
"file:///foo/bar",
),
}: 0,
},
},