Use relative paths for --find-links and local registries (#6566)

## Summary

See: https://github.com/astral-sh/uv/issues/6458
This commit is contained in:
Charlie Marsh 2024-08-24 22:41:47 -04:00 committed by GitHub
parent 3902bb498d
commit 7fa265a11b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 834 additions and 264 deletions

View file

@ -143,8 +143,6 @@ impl Display for FileLocation {
PartialOrd, PartialOrd,
Ord, Ord,
Hash, Hash,
Serialize,
Deserialize,
rkyv::Archive, rkyv::Archive,
rkyv::Deserialize, rkyv::Deserialize,
rkyv::Serialize, rkyv::Serialize,
@ -153,6 +151,24 @@ impl Display for FileLocation {
#[archive_attr(derive(Debug))] #[archive_attr(derive(Debug))]
pub struct UrlString(String); pub struct UrlString(String);
impl serde::Serialize for UrlString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
String::serialize(&self.0, serializer)
}
}
impl<'de> serde::de::Deserialize<'de> for UrlString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
String::deserialize(deserializer).map(UrlString)
}
}
impl UrlString { impl UrlString {
/// Converts a [`UrlString`] to a [`Url`]. /// Converts a [`UrlString`] to a [`Url`].
pub fn to_url(&self) -> Url { pub fn to_url(&self) -> Url {

View file

@ -112,8 +112,8 @@ impl FromStr for IndexUrl {
type Err = IndexUrlError; type Err = IndexUrlError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = if let Ok(path) = Path::new(s).canonicalize() { let url = if Path::new(s).exists() {
VerbatimUrl::from_path(path)? VerbatimUrl::from_absolute_path(std::path::absolute(s)?)?
} else { } else {
VerbatimUrl::parse_url(s)? VerbatimUrl::parse_url(s)?
}; };
@ -247,8 +247,8 @@ impl FromStr for FlatIndexLocation {
type Err = IndexUrlError; type Err = IndexUrlError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = if let Ok(path) = Path::new(s).canonicalize() { let url = if Path::new(s).exists() {
VerbatimUrl::from_path(path)? VerbatimUrl::from_absolute_path(std::path::absolute(s)?)?
} else { } else {
VerbatimUrl::parse_url(s)? VerbatimUrl::parse_url(s)?
}; };

View file

@ -40,11 +40,11 @@ impl UnnamedRequirementUrl for VerbatimUrl {
path: impl AsRef<Path>, path: impl AsRef<Path>,
working_dir: impl AsRef<Path>, working_dir: impl AsRef<Path>,
) -> Result<Self, VerbatimUrlError> { ) -> Result<Self, VerbatimUrlError> {
Self::parse_path(path, working_dir) Self::from_path(path, working_dir)
} }
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> { fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> {
Self::parse_absolute_path(path) Self::from_absolute_path(path)
} }
fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err> { fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err> {

View file

@ -40,14 +40,29 @@ impl VerbatimUrl {
Self { url, given: None } Self { url, given: None }
} }
/// Create a [`VerbatimUrl`] from a file path. /// Parse a URL from a string, expanding any environment variables.
/// pub fn parse_url(given: impl AsRef<str>) -> Result<Self, ParseError> {
/// Assumes that the path is absolute. let url = Url::parse(given.as_ref())?;
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, VerbatimUrlError> { Ok(Self { url, given: 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 from_path(
path: impl AsRef<Path>,
base_dir: impl AsRef<Path>,
) -> Result<Self, VerbatimUrlError> {
debug_assert!(base_dir.as_ref().is_absolute(), "base dir must be absolute");
let path = path.as_ref(); let path = path.as_ref();
// Normalize the path. // Convert the path to an absolute path, if necessary.
let path = normalize_absolute_path(path) let path = if path.is_absolute() {
Cow::Borrowed(path)
} else {
Cow::Owned(base_dir.as_ref().join(path))
};
let path = normalize_absolute_path(&path)
.map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?; .map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?;
// Extract the fragment, if it exists. // Extract the fragment, if it exists.
@ -65,60 +80,20 @@ impl VerbatimUrl {
Ok(Self { url, given: None }) Ok(Self { url, given: 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 })
}
/// 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>,
base_dir: impl AsRef<Path>,
) -> Result<Self, VerbatimUrlError> {
debug_assert!(base_dir.as_ref().is_absolute(), "base dir must be absolute");
let path = path.as_ref();
// Convert the path to an absolute path, if necessary.
let path = if path.is_absolute() {
path.to_path_buf()
} else {
base_dir.as_ref().join(path)
};
let path = normalize_absolute_path(&path)
.map_err(|err| VerbatimUrlError::Normalization(path.clone(), err))?;
// Extract the fragment, if it exists.
let (path, fragment) = split_fragment(&path);
// Convert to a URL.
let mut url = Url::from_file_path(path.clone())
.map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?;
// Set the fragment, if it exists.
if let Some(fragment) = fragment {
url.set_fragment(Some(fragment));
}
Ok(Self { url, given: None })
}
/// Parse a URL from an absolute path. /// Parse a URL from an absolute path.
pub fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, VerbatimUrlError> { pub fn from_absolute_path(path: impl AsRef<Path>) -> Result<Self, VerbatimUrlError> {
let path = path.as_ref(); let path = path.as_ref();
// Convert the path to an absolute path, if necessary. // Error if the path is relative.
let path = if path.is_absolute() { let path = if path.is_absolute() {
path.to_path_buf() path
} else { } else {
return Err(VerbatimUrlError::WorkingDirectory(path.to_path_buf())); return Err(VerbatimUrlError::WorkingDirectory(path.to_path_buf()));
}; };
// Normalize the path. // Normalize the path.
let path = normalize_absolute_path(&path) let path = normalize_absolute_path(path)
.map_err(|err| VerbatimUrlError::Normalization(path.clone(), err))?; .map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?;
// Extract the fragment, if it exists. // Extract the fragment, if it exists.
let (path, fragment) = split_fragment(&path); let (path, fragment) = split_fragment(&path);
@ -252,14 +227,11 @@ impl Pep508Url for VerbatimUrl {
#[cfg(feature = "non-pep508-extensions")] #[cfg(feature = "non-pep508-extensions")]
if let Some(working_dir) = working_dir { if let Some(working_dir) = working_dir {
return Ok(VerbatimUrl::parse_path(path.as_ref(), working_dir)? return Ok(VerbatimUrl::from_path(path.as_ref(), working_dir)?
.with_given(url.to_string())); .with_given(url.to_string()));
} }
Ok( Ok(VerbatimUrl::from_absolute_path(path.as_ref())?.with_given(url.to_string()))
VerbatimUrl::parse_absolute_path(path.as_ref())?
.with_given(url.to_string()),
)
} }
// Ex) `https://download.pytorch.org/whl/torch_stable.html` // Ex) `https://download.pytorch.org/whl/torch_stable.html`
@ -272,11 +244,11 @@ impl Pep508Url for VerbatimUrl {
_ => { _ => {
#[cfg(feature = "non-pep508-extensions")] #[cfg(feature = "non-pep508-extensions")]
if let Some(working_dir) = working_dir { if let Some(working_dir) = working_dir {
return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)? return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)?
.with_given(url.to_string())); .with_given(url.to_string()));
} }
Ok(VerbatimUrl::parse_absolute_path(expanded.as_ref())? Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())?
.with_given(url.to_string())) .with_given(url.to_string()))
} }
} }
@ -284,11 +256,11 @@ impl Pep508Url for VerbatimUrl {
// Ex) `../editable/` // Ex) `../editable/`
#[cfg(feature = "non-pep508-extensions")] #[cfg(feature = "non-pep508-extensions")]
if let Some(working_dir) = working_dir { if let Some(working_dir) = working_dir {
return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)? return Ok(VerbatimUrl::from_path(expanded.as_ref(), working_dir)?
.with_given(url.to_string())); .with_given(url.to_string()));
} }
Ok(VerbatimUrl::parse_absolute_path(expanded.as_ref())?.with_given(url.to_string())) Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())?.with_given(url.to_string()))
} }
} }
} }

View file

@ -60,7 +60,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
path: impl AsRef<Path>, path: impl AsRef<Path>,
working_dir: impl AsRef<Path>, working_dir: impl AsRef<Path>,
) -> Result<Self, Self::Err> { ) -> Result<Self, Self::Err> {
let verbatim = VerbatimUrl::parse_path(&path, &working_dir)?; let verbatim = VerbatimUrl::from_path(&path, &working_dir)?;
let verbatim_path = verbatim.as_path()?; let verbatim_path = verbatim.as_path()?;
let is_dir = if let Ok(metadata) = verbatim_path.metadata() { let is_dir = if let Ok(metadata) = verbatim_path.metadata() {
metadata.is_dir() metadata.is_dir()
@ -89,7 +89,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
} }
fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> { fn parse_absolute_path(path: impl AsRef<Path>) -> Result<Self, Self::Err> {
let verbatim = VerbatimUrl::parse_absolute_path(&path)?; let verbatim = VerbatimUrl::from_absolute_path(&path)?;
let verbatim_path = verbatim.as_path()?; let verbatim_path = verbatim.as_path()?;
let is_dir = if let Ok(metadata) = verbatim_path.metadata() { let is_dir = if let Ok(metadata) = verbatim_path.metadata() {
metadata.is_dir() metadata.is_dir()

View file

@ -507,7 +507,8 @@ impl RequirementSource {
ext, ext,
url, url,
} => Ok(Self::Path { } => Ok(Self::Path {
install_path: relative_to(&install_path, path)?, install_path: relative_to(&install_path, path)
.or_else(|_| std::path::absolute(install_path))?,
ext, ext,
url, url,
}), }),
@ -516,7 +517,8 @@ impl RequirementSource {
editable, editable,
url, url,
} => Ok(Self::Directory { } => Ok(Self::Directory {
install_path: relative_to(&install_path, path)?, install_path: relative_to(&install_path, path)
.or_else(|_| std::path::absolute(install_path))?,
editable, editable,
url, url,
}), }),
@ -744,7 +746,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
// sources in the lockfile, we replace the URL anyway. // sources in the lockfile, we replace the URL anyway.
RequirementSourceWire::Path { path } => { RequirementSourceWire::Path { path } => {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let url = VerbatimUrl::parse_path(&path, &*CWD)?; let url = VerbatimUrl::from_path(&path, &*CWD)?;
Ok(Self::Path { Ok(Self::Path {
ext: DistExtension::from_path(path.as_path()) ext: DistExtension::from_path(path.as_path())
.map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?, .map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
@ -754,7 +756,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
} }
RequirementSourceWire::Directory { directory } => { RequirementSourceWire::Directory { directory } => {
let directory = PathBuf::from(directory); let directory = PathBuf::from(directory);
let url = VerbatimUrl::parse_path(&directory, &*CWD)?; let url = VerbatimUrl::from_path(&directory, &*CWD)?;
Ok(Self::Directory { Ok(Self::Directory {
install_path: directory, install_path: directory,
editable: false, editable: false,
@ -763,7 +765,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
} }
RequirementSourceWire::Editable { editable } => { RequirementSourceWire::Editable { editable } => {
let editable = PathBuf::from(editable); let editable = PathBuf::from(editable);
let url = VerbatimUrl::parse_path(&editable, &*CWD)?; let url = VerbatimUrl::from_path(&editable, &*CWD)?;
Ok(Self::Directory { Ok(Self::Directory {
install_path: editable, install_path: editable,
editable: true, editable: true,
@ -788,7 +790,7 @@ pub fn redact_git_credentials(url: &mut Url) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use pep508_rs::{MarkerTree, VerbatimUrl}; use pep508_rs::{MarkerTree, VerbatimUrl};
@ -823,7 +825,7 @@ mod tests {
source: RequirementSource::Directory { source: RequirementSource::Directory {
install_path: PathBuf::from(path), install_path: PathBuf::from(path),
editable: false, editable: false,
url: VerbatimUrl::from_path(Path::new(path)).unwrap(), url: VerbatimUrl::from_absolute_path(path).unwrap(),
}, },
origin: None, origin: None,
}; };

View file

@ -484,12 +484,17 @@ fn parse_entry(
} else if s.eat_if("-i") || s.eat_if("--index-url") { } else if s.eat_if("-i") || s.eat_if("--index-url") {
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
let expanded = expand_env_vars(given); let expanded = expand_env_vars(given);
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() { let url = if let Some(path) = std::path::absolute(expanded.as_ref())
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl { .ok()
source: err, .filter(|path| path.exists())
url: given.to_string(), {
start, VerbatimUrl::from_absolute_path(path).map_err(|err| {
end: s.cursor(), RequirementsTxtParserError::VerbatimUrl {
source: err,
url: given.to_string(),
start,
end: s.cursor(),
}
})? })?
} else { } else {
VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| { VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| {
@ -505,12 +510,17 @@ fn parse_entry(
} else if s.eat_if("--extra-index-url") { } else if s.eat_if("--extra-index-url") {
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
let expanded = expand_env_vars(given); let expanded = expand_env_vars(given);
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() { let url = if let Some(path) = std::path::absolute(expanded.as_ref())
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl { .ok()
source: err, .filter(|path| path.exists())
url: given.to_string(), {
start, VerbatimUrl::from_absolute_path(path).map_err(|err| {
end: s.cursor(), RequirementsTxtParserError::VerbatimUrl {
source: err,
url: given.to_string(),
start,
end: s.cursor(),
}
})? })?
} else { } else {
VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| { VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| {
@ -528,12 +538,17 @@ fn parse_entry(
} else if s.eat_if("--find-links") || s.eat_if("-f") { } else if s.eat_if("--find-links") || s.eat_if("-f") {
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?; let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
let expanded = expand_env_vars(given); let expanded = expand_env_vars(given);
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() { let url = if let Some(path) = std::path::absolute(expanded.as_ref())
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl { .ok()
source: err, .filter(|path| path.exists())
url: given.to_string(), {
start, VerbatimUrl::from_absolute_path(path).map_err(|err| {
end: s.cursor(), RequirementsTxtParserError::VerbatimUrl {
source: err,
url: given.to_string(),
start,
end: s.cursor(),
}
})? })?
} else { } else {
VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| { VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| {

View file

@ -142,7 +142,7 @@ impl LoweredRequirement {
// relative to workspace: `packages/current_project` // relative to workspace: `packages/current_project`
// workspace lock root: `../current_workspace` // workspace lock root: `../current_workspace`
// relative to main workspace: `../current_workspace/packages/current_project` // relative to main workspace: `../current_workspace/packages/current_project`
let url = VerbatimUrl::parse_absolute_path(member.root())?; let url = VerbatimUrl::from_absolute_path(member.root())?;
let install_path = url.to_file_path().map_err(|()| { let install_path = url.to_file_path().map_err(|()| {
LoweringError::RelativeTo(io::Error::new( LoweringError::RelativeTo(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
@ -360,7 +360,7 @@ fn path_source(
Origin::Project => project_dir, Origin::Project => project_dir,
Origin::Workspace => workspace_root, Origin::Workspace => workspace_root,
}; };
let url = VerbatimUrl::parse_path(path, base)?.with_given(path.to_string_lossy()); let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy());
let install_path = url.to_file_path().map_err(|()| { let install_path = url.to_file_path().map_err(|()| {
LoweringError::RelativeTo(io::Error::new( LoweringError::RelativeTo(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,

View file

@ -270,6 +270,9 @@ pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBu
/// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py`
/// `lib/marker.txt` and `lib/python/site-packages` -> `../../marker.txt` /// `lib/marker.txt` and `lib/python/site-packages` -> `../../marker.txt`
/// `bin/foo_launcher` and `lib/python/site-packages` -> `../../../bin/foo_launcher` /// `bin/foo_launcher` and `lib/python/site-packages` -> `../../../bin/foo_launcher`
///
/// Returns `Err` if there is no relative path between `path` and `base` (for example, if the paths
/// are on different drives on Windows).
pub fn relative_to( pub fn relative_to(
path: impl AsRef<Path>, path: impl AsRef<Path>,
base: impl AsRef<Path>, base: impl AsRef<Path>,

View file

@ -23,7 +23,7 @@ use distribution_types::{
UrlString, UrlString,
}; };
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError};
use platform_tags::{TagCompatibility, TagPriority, Tags}; use platform_tags::{TagCompatibility, TagPriority, Tags};
use pypi_types::{ use pypi_types::{
redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement,
@ -762,12 +762,55 @@ impl Lock {
} }
// Collect the set of available indexes (both `--index-url` and `--find-links` entries). // Collect the set of available indexes (both `--index-url` and `--find-links` entries).
let indexes = indexes.map(|locations| { let remotes = indexes.map(|locations| {
locations locations
.indexes() .indexes()
.map(IndexUrl::redacted) .filter_map(|index_url| match index_url {
.chain(locations.flat_index().map(FlatIndexLocation::redacted)) IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
.map(UrlString::from) Some(UrlString::from(index_url.redacted()))
}
IndexUrl::Path(_) => None,
})
.chain(
locations
.flat_index()
.filter_map(|index_url| match index_url {
FlatIndexLocation::Url(_) => {
Some(UrlString::from(index_url.redacted()))
}
FlatIndexLocation::Path(_) => None,
}),
)
.collect::<BTreeSet<_>>()
});
let locals = indexes.map(|locations| {
locations
.indexes()
.filter_map(|index_url| match index_url {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
IndexUrl::Path(index_url) => {
let path = index_url.to_file_path().ok()?;
let path = relative_to(&path, workspace.install_path())
.or_else(|_| std::path::absolute(path))
.ok()?;
Some(path)
}
})
.chain(
locations
.flat_index()
.filter_map(|index_url| match index_url {
FlatIndexLocation::Url(_) => None,
FlatIndexLocation::Path(index_url) => {
let path = index_url.to_file_path().ok()?;
let path = relative_to(&path, workspace.install_path())
.or_else(|_| std::path::absolute(path))
.ok()?;
Some(path)
}
}),
)
.collect::<BTreeSet<_>>() .collect::<BTreeSet<_>>()
}); });
@ -789,16 +832,29 @@ impl Lock {
while let Some(package) = queue.pop_front() { while let Some(package) = queue.pop_front() {
// If the lockfile references an index that was not provided, we can't validate it. // If the lockfile references an index that was not provided, we can't validate it.
if let Source::Registry(index) = &package.id.source { if let Source::Registry(index) = &package.id.source {
if indexes match index {
.as_ref() RegistrySource::Url(url) => {
.is_some_and(|indexes| !indexes.contains(index)) if remotes
{ .as_ref()
return Ok(SatisfiesResult::MissingIndex( .is_some_and(|remotes| !remotes.contains(url))
&package.id.name, {
&package.id.version, return Ok(SatisfiesResult::MissingRemoteIndex(
index, &package.id.name,
)); &package.id.version,
} url,
));
}
}
RegistrySource::Path(path) => {
if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
return Ok(SatisfiesResult::MissingLocalIndex(
&package.id.name,
&package.id.version,
path,
));
}
}
};
} }
// If the package is immutable, we don't need to validate it (or its dependencies). // If the package is immutable, we don't need to validate it (or its dependencies).
@ -935,8 +991,10 @@ pub enum SatisfiesResult<'lock> {
MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>), MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
/// The lockfile is missing a workspace member. /// The lockfile is missing a workspace member.
MissingRoot(PackageName), MissingRoot(PackageName),
/// The lockfile referenced an index that was not provided /// The lockfile referenced a remote index that was not provided
MissingIndex(&'lock PackageName, &'lock Version, &'lock UrlString), MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
/// The lockfile referenced a local index that was not provided
MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock PathBuf),
/// The resolver failed to generate metadata for a given package. /// The resolver failed to generate metadata for a given package.
MissingMetadata(&'lock PackageName, &'lock Version), MissingMetadata(&'lock PackageName, &'lock Version),
/// A package in the lockfile contains different `requires-dist` metadata than expected. /// A package in the lockfile contains different `requires-dist` metadata than expected.
@ -1241,11 +1299,11 @@ impl Package {
fn to_dist(&self, workspace_root: &Path, 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) { if let Some(best_wheel_index) = self.find_best_wheel(tags) {
return match &self.id.source { return match &self.id.source {
Source::Registry(url) => { Source::Registry(source) => {
let wheels = self let wheels = self
.wheels .wheels
.iter() .iter()
.map(|wheel| wheel.to_registry_dist(url.to_url())) .map(|wheel| wheel.to_registry_dist(source, workspace_root))
.collect::<Result<_, LockError>>()?; .collect::<Result<_, LockError>>()?;
let reg_built_dist = RegistryBuiltDist { let reg_built_dist = RegistryBuiltDist {
wheels, wheels,
@ -1295,7 +1353,7 @@ impl Package {
} }
.into()), .into()),
}; };
} };
if let Some(sdist) = self.to_source_dist(workspace_root)? { if let Some(sdist) = self.to_source_dist(workspace_root)? {
return Ok(Dist::Source(sdist)); return Ok(Dist::Source(sdist));
@ -1388,7 +1446,7 @@ impl Package {
}; };
distribution_types::SourceDist::DirectUrl(direct_dist) distribution_types::SourceDist::DirectUrl(direct_dist)
} }
Source::Registry(url) => { Source::Registry(RegistrySource::Url(url)) => {
let Some(ref sdist) = self.sdist else { let Some(ref sdist) = self.sdist else {
return Ok(None); return Ok(None);
}; };
@ -1418,6 +1476,51 @@ impl Package {
}); });
let index = IndexUrl::Url(VerbatimUrl::from_url(url.to_url())); let index = IndexUrl::Url(VerbatimUrl::from_url(url.to_url()));
let reg_dist = RegistrySourceDist {
name: self.id.name.clone(),
version: self.id.version.clone(),
file,
ext,
index,
wheels: vec![],
};
distribution_types::SourceDist::Registry(reg_dist)
}
Source::Registry(RegistrySource::Path(path)) => {
let Some(ref sdist) = self.sdist else {
return Ok(None);
};
let file_path = sdist.path().ok_or_else(|| LockErrorKind::MissingPath {
name: self.id.name.clone(),
version: self.id.version.clone(),
})?;
let file_url = Url::from_file_path(workspace_root.join(path).join(file_path))
.map_err(|()| LockErrorKind::PathToUrl)?;
let filename = sdist
.filename()
.ok_or_else(|| LockErrorKind::MissingFilename {
id: self.id.clone(),
})?;
let ext = SourceDistExtension::from_path(filename.as_ref())?;
let file = Box::new(distribution_types::File {
dist_info_metadata: false,
filename: filename.to_string(),
hashes: sdist
.hash()
.map(|hash| vec![hash.0.clone()])
.unwrap_or_default(),
requires_python: None,
size: sdist.size(),
upload_time_utc_ms: None,
url: FileLocation::AbsoluteUrl(UrlString::from(file_url)),
yanked: None,
});
let index = IndexUrl::Path(
VerbatimUrl::from_absolute_path(workspace_root.join(path))
.map_err(LockErrorKind::RegistryVerbatimUrl)?,
);
let reg_dist = RegistrySourceDist { let reg_dist = RegistrySourceDist {
name: self.id.name.clone(), name: self.id.name.clone(),
version: self.id.version.clone(), version: self.id.version.clone(),
@ -1597,14 +1700,6 @@ impl Package {
self.fork_markers.as_slice() self.fork_markers.as_slice()
} }
/// Return the index URL for this package, if it is a registry source.
pub fn index(&self) -> Option<&UrlString> {
match &self.id.source {
Source::Registry(url) => Some(url),
_ => None,
}
}
/// Returns all the hashes associated with this [`Package`]. /// Returns all the hashes associated with this [`Package`].
fn hashes(&self) -> Vec<HashDigest> { fn hashes(&self) -> Vec<HashDigest> {
let mut hashes = Vec::new(); let mut hashes = Vec::new();
@ -1636,7 +1731,7 @@ impl Package {
/// Attempts to construct a `VerbatimUrl` from the given `Path`. /// Attempts to construct a `VerbatimUrl` from the given `Path`.
fn verbatim_url(path: PathBuf, id: &PackageId) -> Result<VerbatimUrl, LockError> { fn verbatim_url(path: PathBuf, id: &PackageId) -> Result<VerbatimUrl, LockError> {
let url = VerbatimUrl::from_path(path).map_err(|err| LockErrorKind::VerbatimUrl { let url = VerbatimUrl::from_absolute_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
id: id.clone(), id: id.clone(),
err, err,
})?; })?;
@ -1836,11 +1931,17 @@ impl From<PackageId> for PackageIdForDependency {
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)] #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
#[serde(try_from = "SourceWire")] #[serde(try_from = "SourceWire")]
enum Source { enum Source {
Registry(UrlString), /// A registry or `--find-links` index.
Registry(RegistrySource),
/// A Git repository.
Git(UrlString, GitSource), Git(UrlString, GitSource),
/// A direct HTTP(S) URL.
Direct(UrlString, DirectSource), Direct(UrlString, DirectSource),
/// A path to a local source or built archive.
Path(PathBuf), Path(PathBuf),
/// A path to a local directory.
Directory(PathBuf), Directory(PathBuf),
/// A path to a local directory that should be installed as editable.
Editable(PathBuf), Editable(PathBuf),
} }
@ -1862,7 +1963,7 @@ impl Source {
fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Source, LockError> { fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Source, LockError> {
match *built_dist { match *built_dist {
BuiltDist::Registry(ref reg_dist) => Ok(Source::from_registry_built_dist(reg_dist)), BuiltDist::Registry(ref reg_dist) => Source::from_registry_built_dist(reg_dist, root),
BuiltDist::DirectUrl(ref direct_dist) => { BuiltDist::DirectUrl(ref direct_dist) => {
Ok(Source::from_direct_built_dist(direct_dist)) Ok(Source::from_direct_built_dist(direct_dist))
} }
@ -1876,7 +1977,7 @@ impl Source {
) -> Result<Source, LockError> { ) -> Result<Source, LockError> {
match *source_dist { match *source_dist {
distribution_types::SourceDist::Registry(ref reg_dist) => { distribution_types::SourceDist::Registry(ref reg_dist) => {
Ok(Source::from_registry_source_dist(reg_dist)) Source::from_registry_source_dist(reg_dist, root)
} }
distribution_types::SourceDist::DirectUrl(ref direct_dist) => { distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
Ok(Source::from_direct_source_dist(direct_dist)) Ok(Source::from_direct_source_dist(direct_dist))
@ -1893,12 +1994,18 @@ impl Source {
} }
} }
fn from_registry_built_dist(reg_dist: &RegistryBuiltDist) -> Source { fn from_registry_built_dist(
Source::from_index_url(&reg_dist.best_wheel().index) reg_dist: &RegistryBuiltDist,
root: &Path,
) -> Result<Source, LockError> {
Source::from_index_url(&reg_dist.best_wheel().index, root)
} }
fn from_registry_source_dist(reg_dist: &RegistrySourceDist) -> Source { fn from_registry_source_dist(
Source::from_index_url(&reg_dist.index) reg_dist: &RegistrySourceDist,
root: &Path,
) -> Result<Source, LockError> {
Source::from_index_url(&reg_dist.index, root)
} }
fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Source { fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Source {
@ -1923,14 +2030,15 @@ impl Source {
fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Source, LockError> { fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Source, LockError> {
let path = relative_to(&path_dist.install_path, root) let path = relative_to(&path_dist.install_path, root)
.or_else(|_| std::path::absolute(&path_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?; .map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Source::Path(path)) Ok(Source::Path(path))
} }
fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Source, LockError> { fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Source, LockError> {
let path = relative_to(&path_dist.install_path, root) let path = relative_to(&path_dist.install_path, root)
.or_else(|_| std::path::absolute(&path_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?; .map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Source::Path(path)) Ok(Source::Path(path))
} }
@ -1939,6 +2047,7 @@ impl Source {
root: &Path, root: &Path,
) -> Result<Source, LockError> { ) -> Result<Source, LockError> {
let path = relative_to(&directory_dist.install_path, root) let path = relative_to(&directory_dist.install_path, root)
.or_else(|_| std::path::absolute(&directory_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?; .map_err(LockErrorKind::DistributionRelativePath)?;
if directory_dist.editable { if directory_dist.editable {
Ok(Source::Editable(path)) Ok(Source::Editable(path))
@ -1947,10 +2056,23 @@ impl Source {
} }
} }
fn from_index_url(index_url: &IndexUrl) -> Source { fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Source, LockError> {
// Remove any sensitive credentials from the index URL. match index_url {
let redacted = index_url.redacted(); IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
Source::Registry(UrlString::from(redacted.as_ref())) // Remove any sensitive credentials from the index URL.
let redacted = index_url.redacted();
let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
Ok(Source::Registry(source))
}
IndexUrl::Path(url) => {
let path = url.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?;
let path = relative_to(&path, root)
.or_else(|_| std::path::absolute(&path))
.map_err(LockErrorKind::IndexRelativePath)?;
let source = RegistrySource::Path(path);
Ok(Source::Registry(source))
}
}
} }
fn from_git_dist(git_dist: &GitSourceDist) -> Source { fn from_git_dist(git_dist: &GitSourceDist) -> Source {
@ -1983,9 +2105,17 @@ impl Source {
fn to_toml(&self, table: &mut Table) { fn to_toml(&self, table: &mut Table) {
let mut source_table = InlineTable::new(); let mut source_table = InlineTable::new();
match *self { match *self {
Source::Registry(ref url) => { Source::Registry(ref source) => match source {
source_table.insert("registry", Value::from(url.as_ref())); RegistrySource::Url(url) => {
} source_table.insert("registry", Value::from(url.as_ref()));
}
RegistrySource::Path(path) => {
source_table.insert(
"registry",
Value::from(PortablePath::from(path).to_string()),
);
}
},
Source::Git(ref url, _) => { Source::Git(ref url, _) => {
source_table.insert("git", Value::from(url.as_ref())); source_table.insert("git", Value::from(url.as_ref()));
} }
@ -2018,10 +2148,15 @@ impl Source {
impl std::fmt::Display for Source { impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {
Source::Registry(url) | Source::Git(url, _) | Source::Direct(url, _) => { Source::Registry(RegistrySource::Url(url))
| Source::Git(url, _)
| Source::Direct(url, _) => {
write!(f, "{}+{}", self.name(), url) write!(f, "{}+{}", self.name(), url)
} }
Source::Path(path) | Source::Directory(path) | Source::Editable(path) => { Source::Registry(RegistrySource::Path(path))
| Source::Path(path)
| Source::Directory(path)
| Source::Editable(path) => {
write!(f, "{}+{}", self.name(), PortablePath::from(path)) write!(f, "{}+{}", self.name(), PortablePath::from(path))
} }
} }
@ -2060,7 +2195,7 @@ impl Source {
#[serde(untagged)] #[serde(untagged)]
enum SourceWire { enum SourceWire {
Registry { Registry {
registry: UrlString, registry: RegistrySource,
}, },
Git { Git {
git: String, git: String,
@ -2119,6 +2254,64 @@ impl TryFrom<SourceWire> for Source {
} }
} }
/// The source for a registry, which could be a URL or a relative path.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
enum RegistrySource {
/// Ex) `https://pypi.org/simple`
Url(UrlString),
/// Ex) `../path/to/local/index`
Path(PathBuf),
}
impl std::fmt::Display for RegistrySource {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RegistrySource::Url(url) => write!(f, "{url}"),
RegistrySource::Path(path) => write!(f, "{}", path.display()),
}
}
}
impl<'de> serde::de::Deserialize<'de> for RegistrySource {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = RegistrySource;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a valid URL or a file path")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if split_scheme(value).is_some() {
Ok(
serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
value,
))
.map(RegistrySource::Url)?,
)
} else {
Ok(
serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
value,
))
.map(RegistrySource::Path)?,
)
}
}
}
deserializer.deserialize_str(Visitor)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)] #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
struct DirectSource { struct DirectSource {
subdirectory: Option<String>, subdirectory: Option<String>,
@ -2223,6 +2416,13 @@ impl SourceDist {
} }
} }
fn path(&self) -> Option<&Path> {
match &self {
SourceDist::Url { .. } => None,
SourceDist::Path { path, .. } => Some(path),
}
}
fn hash(&self) -> Option<&Hash> { fn hash(&self) -> Option<&Hash> {
match &self { match &self {
SourceDist::Url { metadata, .. } => metadata.hash.as_ref(), SourceDist::Url { metadata, .. } => metadata.hash.as_ref(),
@ -2304,15 +2504,38 @@ impl SourceDist {
return Ok(None); return Ok(None);
} }
let url = normalize_file_location(&reg_dist.file.url) match &reg_dist.index {
.map_err(LockErrorKind::InvalidFileUrl) IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
.map_err(LockError::from)?; let url = normalize_file_location(&reg_dist.file.url)
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); .map_err(LockErrorKind::InvalidFileUrl)
let size = reg_dist.file.size; .map_err(LockError::from)?;
Ok(Some(SourceDist::Url { let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
url, let size = reg_dist.file.size;
metadata: SourceDistMetadata { hash, size }, Ok(Some(SourceDist::Url {
})) url,
metadata: SourceDistMetadata { hash, size },
}))
}
IndexUrl::Path(path) => {
let index_path = path.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?;
let reg_dist_path = reg_dist
.file
.url
.to_url()
.map_err(LockErrorKind::InvalidFileUrl)?
.to_file_path()
.map_err(|()| LockErrorKind::UrlToPath)?;
let path = relative_to(&reg_dist_path, index_path)
.or_else(|_| std::path::absolute(&reg_dist_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
let size = reg_dist.file.size;
Ok(Some(SourceDist::Path {
path,
metadata: SourceDistMetadata { hash, size },
}))
}
}
} }
fn from_direct_dist( fn from_direct_dist(
@ -2558,17 +2781,40 @@ impl Wheel {
fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Wheel, LockError> { fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Wheel, LockError> {
let filename = wheel.filename.clone(); let filename = wheel.filename.clone();
let url = normalize_file_location(&wheel.file.url) match &wheel.index {
.map_err(LockErrorKind::InvalidFileUrl) IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
.map_err(LockError::from)?; let url = normalize_file_location(&wheel.file.url)
let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); .map_err(LockErrorKind::InvalidFileUrl)
let size = wheel.file.size; .map_err(LockError::from)?;
Ok(Wheel { let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
url: WheelWireSource::Url { url }, let size = wheel.file.size;
hash, Ok(Wheel {
size, url: WheelWireSource::Url { url },
filename, hash,
}) size,
filename,
})
}
IndexUrl::Path(path) => {
let index_path = path.to_file_path().map_err(|()| LockErrorKind::UrlToPath)?;
let wheel_path = wheel
.file
.url
.to_url()
.map_err(LockErrorKind::InvalidFileUrl)?
.to_file_path()
.map_err(|()| LockErrorKind::UrlToPath)?;
let path = relative_to(&wheel_path, index_path)
.or_else(|_| std::path::absolute(&wheel_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Wheel {
url: WheelWireSource::Path { path },
hash: None,
size: None,
filename,
})
}
}
} }
fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Wheel { fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Wheel {
@ -2593,34 +2839,76 @@ impl Wheel {
} }
} }
fn to_registry_dist(&self, url: Url) -> Result<RegistryBuiltWheel, LockError> { fn to_registry_dist(
&self,
source: &RegistrySource,
root: &Path,
) -> Result<RegistryBuiltWheel, LockError> {
let filename: WheelFilename = self.filename.clone(); let filename: WheelFilename = self.filename.clone();
let url_string = match &self.url {
WheelWireSource::Url { url } => url.clone(), match source {
WheelWireSource::Filename { .. } => { RegistrySource::Url(index_url) => {
return Err(LockErrorKind::MissingUrl { let file_url = match &self.url {
name: self.filename.name.clone(), WheelWireSource::Url { url } => url,
version: self.filename.version.clone(), WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
} return Err(LockErrorKind::MissingUrl {
.into()) name: filename.name,
version: filename.version,
}
.into())
}
};
let file = Box::new(distribution_types::File {
dist_info_metadata: false,
filename: filename.to_string(),
hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
requires_python: None,
size: self.size,
upload_time_utc_ms: None,
url: FileLocation::AbsoluteUrl(file_url.clone()),
yanked: None,
});
let index = IndexUrl::Url(VerbatimUrl::from_url(index_url.to_url()));
Ok(RegistryBuiltWheel {
filename,
file,
index,
})
} }
}; RegistrySource::Path(index_path) => {
let file = Box::new(distribution_types::File { let file_path = match &self.url {
dist_info_metadata: false, WheelWireSource::Path { path } => path,
filename: filename.to_string(), WheelWireSource::Url { .. } | WheelWireSource::Filename { .. } => {
hashes: self.hash.iter().map(|h| h.0.clone()).collect(), return Err(LockErrorKind::MissingPath {
requires_python: None, name: filename.name,
size: self.size, version: filename.version,
upload_time_utc_ms: None, }
url: FileLocation::AbsoluteUrl(url_string), .into())
yanked: None, }
}); };
let index = IndexUrl::Url(VerbatimUrl::from_url(url)); let file_url = Url::from_file_path(root.join(index_path).join(file_path))
Ok(RegistryBuiltWheel { .map_err(|()| LockErrorKind::PathToUrl)?;
filename, let file = Box::new(distribution_types::File {
file, dist_info_metadata: false,
index, filename: filename.to_string(),
}) hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
requires_python: None,
size: self.size,
upload_time_utc_ms: None,
url: FileLocation::AbsoluteUrl(UrlString::from(file_url)),
yanked: None,
});
let index = IndexUrl::Path(
VerbatimUrl::from_absolute_path(root.join(index_path))
.map_err(LockErrorKind::RegistryVerbatimUrl)?,
);
Ok(RegistryBuiltWheel {
filename,
file,
index,
})
}
}
} }
} }
@ -2643,14 +2931,19 @@ struct WheelWire {
#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
#[serde(untagged)] #[serde(untagged)]
enum WheelWireSource { enum WheelWireSource {
/// Used for all wheels except path wheels. /// Used for all wheels that come from remote sources.
Url { Url {
/// A URL or file path (via `file://`) where the wheel that was locked /// A URL where the wheel that was locked against was found. The location
/// against was found. The location does not need to exist in the future, /// does not need to exist in the future, so this should be treated as
/// so this should be treated as only a hint to where to look and/or /// only a hint to where to look and/or recording where the wheel file
/// recording where the wheel file originally came from. /// originally came from.
url: UrlString, url: UrlString,
}, },
/// Used for wheels that come from local registries (like `--find-links`).
Path {
/// The path to the wheel, relative to the index.
path: PathBuf,
},
/// Used for path wheels. /// Used for path wheels.
/// ///
/// We only store the filename for path wheel, since we can't store a relative path in the url /// We only store the filename for path wheel, since we can't store a relative path in the url
@ -2669,6 +2962,9 @@ impl Wheel {
WheelWireSource::Url { url } => { WheelWireSource::Url { url } => {
table.insert("url", Value::from(url.as_ref())); table.insert("url", Value::from(url.as_ref()));
} }
WheelWireSource::Path { path } => {
table.insert("path", Value::from(PortablePath::from(path).to_string()));
}
WheelWireSource::Filename { filename } => { WheelWireSource::Filename { filename } => {
table.insert("filename", Value::from(filename.to_string())); table.insert("filename", Value::from(filename.to_string()));
} }
@ -2687,7 +2983,6 @@ impl TryFrom<WheelWire> for Wheel {
type Error = String; type Error = String;
fn try_from(wire: WheelWire) -> Result<Wheel, String> { fn try_from(wire: WheelWire) -> Result<Wheel, String> {
// If necessary, extract the filename from the URL.
let filename = match &wire.url { let filename = match &wire.url {
WheelWireSource::Url { url } => { WheelWireSource::Url { url } => {
let filename = url.filename().map_err(|err| err.to_string())?; let filename = url.filename().map_err(|err| err.to_string())?;
@ -2695,6 +2990,17 @@ impl TryFrom<WheelWire> for Wheel {
format!("failed to parse `{filename}` as wheel filename: {err}") format!("failed to parse `{filename}` as wheel filename: {err}")
})? })?
} }
WheelWireSource::Path { path } => {
let filename = path
.file_name()
.and_then(|file_name| file_name.to_str())
.ok_or_else(|| {
format!("path `{}` has no filename component", path.display())
})?;
filename.parse::<WheelFilename>().map_err(|err| {
format!("failed to parse `{filename}` as wheel filename: {err}")
})?
}
WheelWireSource::Filename { filename } => filename.clone(), WheelWireSource::Filename { filename } => filename.clone(),
}; };
@ -2914,7 +3220,7 @@ fn normalize_requirement(
url: _, url: _,
} => { } => {
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path)); let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path));
let url = VerbatimUrl::from_path(&install_path) let url = VerbatimUrl::from_absolute_path(&install_path)
.map_err(LockErrorKind::RequirementVerbatimUrl)?; .map_err(LockErrorKind::RequirementVerbatimUrl)?;
Ok(Requirement { Ok(Requirement {
@ -2935,7 +3241,7 @@ fn normalize_requirement(
url: _, url: _,
} => { } => {
let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path)); let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path));
let url = VerbatimUrl::from_path(&install_path) let url = VerbatimUrl::from_absolute_path(&install_path)
.map_err(LockErrorKind::RequirementVerbatimUrl)?; .map_err(LockErrorKind::RequirementVerbatimUrl)?;
Ok(Requirement { Ok(Requirement {
@ -3098,8 +3404,8 @@ enum LockErrorKind {
/// The kind of the invalid source. /// The kind of the invalid source.
source_type: &'static str, source_type: &'static str,
}, },
/// An error that occurs when a distribution indicates that it is sourced from a registry, but /// An error that occurs when a distribution indicates that it is sourced from a remote
/// is missing a URL. /// registry, but is missing a URL.
#[error("found registry distribution {name}=={version} without a valid URL")] #[error("found registry distribution {name}=={version} without a valid URL")]
MissingUrl { MissingUrl {
/// The name of the distribution that is missing a URL. /// The name of the distribution that is missing a URL.
@ -3107,6 +3413,15 @@ enum LockErrorKind {
/// The version of the distribution that is missing a URL. /// The version of the distribution that is missing a URL.
version: Version, version: Version,
}, },
/// An error that occurs when a distribution indicates that it is sourced from a local registry,
/// but is missing a path.
#[error("found registry distribution {name}=={version} without a valid path")]
MissingPath {
/// The name of the distribution that is missing a path.
name: PackageName,
/// The version of the distribution that is missing a path.
version: Version,
},
/// An error that occurs when a distribution indicates that it is sourced from a registry, but /// An error that occurs when a distribution indicates that it is sourced from a registry, but
/// is missing a filename. /// is missing a filename.
#[error("found registry distribution {id} without a valid filename")] #[error("found registry distribution {id} without a valid filename")]
@ -3137,6 +3452,13 @@ enum LockErrorKind {
#[source] #[source]
std::io::Error, std::io::Error,
), ),
/// An error that occurs when converting an index URL to a relative path
#[error("could not compute relative path between workspace and index")]
IndexRelativePath(
/// The inner error we forward.
#[source]
std::io::Error,
),
/// An error that occurs when an ambiguous `package.dependency` is /// An error that occurs when an ambiguous `package.dependency` is
/// missing a `version` field. /// missing a `version` field.
#[error( #[error(
@ -3171,6 +3493,19 @@ enum LockErrorKind {
#[source] #[source]
VerbatimUrlError, VerbatimUrlError,
), ),
/// An error that occurs when parsing a registry's index URL.
#[error("could not convert between URL and path")]
RegistryVerbatimUrl(
/// The inner error we forward.
#[source]
VerbatimUrlError,
),
/// An error that occurs when converting a path to a URL.
#[error("failed to convert path to URL")]
PathToUrl,
/// An error that occurs when converting a URL to a path
#[error("failed to convert URL to path")]
UrlToPath,
} }
/// An error that occurs when a source string could not be parsed. /// An error that occurs when a source string could not be parsed.

View file

@ -21,8 +21,10 @@ Ok(
), ),
version: "4.3.0", version: "4.3.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -71,8 +73,10 @@ Ok(
), ),
version: "4.3.0", version: "4.3.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 0, }: 0,

View file

@ -21,8 +21,10 @@ Ok(
), ),
version: "4.3.0", version: "4.3.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -78,8 +80,10 @@ Ok(
), ),
version: "4.3.0", version: "4.3.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 0, }: 0,

View file

@ -21,8 +21,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -63,8 +65,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -98,8 +102,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -122,8 +128,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 0, }: 0,
@ -133,8 +141,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 1, }: 1,

View file

@ -21,8 +21,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -63,8 +65,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -98,8 +102,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -122,8 +128,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 0, }: 0,
@ -133,8 +141,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 1, }: 1,

View file

@ -21,8 +21,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -63,8 +65,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -98,8 +102,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}, },
@ -122,8 +128,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 0, }: 0,
@ -133,8 +141,10 @@ Ok(
), ),
version: "0.1.0", version: "0.1.0",
source: Registry( source: Registry(
UrlString( Url(
"https://pypi.org/simple", UrlString(
"https://pypi.org/simple",
),
), ),
), ),
}: 1, }: 1,

View file

@ -270,7 +270,7 @@ impl Workspace {
}) })
.unwrap_or_default(); .unwrap_or_default();
let url = VerbatimUrl::from_path(&member.root) let url = VerbatimUrl::from_absolute_path(&member.root)
.expect("path is valid URL") .expect("path is valid URL")
.with_given(member.root.to_string_lossy()); .with_given(member.root.to_string_lossy());
Some(Requirement { Some(Requirement {

View file

@ -723,9 +723,15 @@ impl ValidatedLock {
debug!("Ignoring existing lockfile due to missing root package: `{name}`"); debug!("Ignoring existing lockfile due to missing root package: `{name}`");
Ok(Self::Preferable(lock)) Ok(Self::Preferable(lock))
} }
SatisfiesResult::MissingIndex(name, version, index) => { SatisfiesResult::MissingRemoteIndex(name, version, index) => {
debug!( debug!(
"Ignoring existing lockfile due to missing index: `{name}` `{version}` from `{index}`" "Ignoring existing lockfile due to missing remote index: `{name}` `{version}` from `{index}`"
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingLocalIndex(name, version, index) => {
debug!(
"Ignoring existing lockfile due to missing local index: `{name}` `{version}` from `{}`", index.display()
); );
Ok(Self::Preferable(lock)) Ok(Self::Preferable(lock))
} }

View file

@ -6493,7 +6493,28 @@ fn lock_warn_missing_transitive_lower_bounds() -> Result<()> {
fn lock_find_links_local_wheel() -> Result<()> { fn lock_find_links_local_wheel() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml"); // Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#" pyproject_toml.write_str(&formatdoc! { r#"
[project] [project]
name = "project" name = "project"
@ -6504,19 +6525,20 @@ fn lock_find_links_local_wheel() -> Result<()> {
[tool.uv] [tool.uv]
find-links = ["{}"] find-links = ["{}"]
"#, "#,
context.workspace_root.join("scripts/links/").portable_display(), context.temp_dir.join("links/").portable_display(),
})?; })?;
uv_snapshot!(context.filters(), context.lock(), @r###" uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
"###); "###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({ insta::with_settings!({
filters => context.filters(), filters => context.filters(),
@ -6543,34 +6565,37 @@ fn lock_find_links_local_wheel() -> Result<()> {
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "1000.0.0" version = "1000.0.0"
source = { registry = "file://[WORKSPACE]/scripts/links" } source = { registry = "../links" }
wheels = [ wheels = [
{ url = "file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl" }, { path = "tqdm-1000.0.0-py3-none-any.whl" },
] ]
"### "###
); );
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Prepared 1 package in [TIME] Prepared 1 package in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/workspace)
+ tqdm==1000.0.0 + tqdm==1000.0.0
"###); "###);
@ -6582,7 +6607,28 @@ fn lock_find_links_local_wheel() -> Result<()> {
fn lock_find_links_local_sdist() -> Result<()> { fn lock_find_links_local_sdist() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml"); // Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#" pyproject_toml.write_str(&formatdoc! { r#"
[project] [project]
name = "project" name = "project"
@ -6593,19 +6639,20 @@ fn lock_find_links_local_sdist() -> Result<()> {
[tool.uv] [tool.uv]
find-links = ["{}"] find-links = ["{}"]
"#, "#,
context.workspace_root.join("scripts/links/").portable_display(), context.temp_dir.join("links/").portable_display(),
})?; })?;
uv_snapshot!(context.filters(), context.lock(), @r###" uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
"###); "###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({ insta::with_settings!({
filters => context.filters(), filters => context.filters(),
@ -6632,32 +6679,35 @@ fn lock_find_links_local_sdist() -> Result<()> {
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "999.0.0" version = "999.0.0"
source = { registry = "file://[WORKSPACE]/scripts/links" } source = { registry = "../links" }
sdist = { url = "file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz" } sdist = { path = "tqdm-999.0.0.tar.gz" }
"### "###
); );
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&workspace), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/workspace)
+ tqdm==999.0.0 + tqdm==999.0.0
"###); "###);
@ -6857,6 +6907,14 @@ fn lock_local_index() -> Result<()> {
let tqdm = root.child("tqdm"); let tqdm = root.child("tqdm");
fs_err::create_dir_all(&tqdm)?; fs_err::create_dir_all(&tqdm)?;
let wheel = tqdm.child("tqdm-1000.0.0-py3-none-any.whl");
fs_err::copy(
context
.workspace_root
.join("scripts/links/tqdm-1000.0.0-py3-none-any.whl"),
&wheel,
)?;
let index = tqdm.child("index.html"); let index = tqdm.child("index.html");
index.write_str(&formatdoc! {r#" index.write_str(&formatdoc! {r#"
<!DOCTYPE html> <!DOCTYPE html>
@ -6867,14 +6925,14 @@ fn lock_local_index() -> Result<()> {
<body> <body>
<h1>Links for tqdm</h1> <h1>Links for tqdm</h1>
<a <a
href="{}/tqdm-1000.0.0-py3-none-any.whl" href="{}"
data-requires-python=">=3.8" data-requires-python=">=3.8"
> >
tqdm-1000.0.0-py3-none-any.whl tqdm-1000.0.0-py3-none-any.whl
</a> </a>
</body> </body>
</html> </html>
"#, Url::from_directory_path(context.workspace_root.join("scripts/links/")).unwrap().as_str()})?; "#, Url::from_file_path(wheel).unwrap().as_str()})?;
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
@ -6931,9 +6989,9 @@ fn lock_local_index() -> Result<()> {
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "1000.0.0" version = "1000.0.0"
source = { registry = "file://[TMP]" } source = { registry = "../../[TMP]/simple-html" }
wheels = [ wheels = [
{ url = "file://[WORKSPACE]/scripts/links//tqdm-1000.0.0-py3-none-any.whl" }, { path = "tqdm/tqdm-1000.0.0-py3-none-any.whl" },
] ]
"### "###
); );
@ -10423,3 +10481,128 @@ fn lock_dropped_dev_extra() -> Result<()> {
Ok(()) Ok(())
} }
/// Use a trailing slash on the declared index.
#[test]
fn lock_trailing_slash() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
[tool.uv]
index-url = "https://pypi.org/simple/"
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "anyio"
version = "3.7.0"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
]
[package.metadata]
requires-dist = [{ name = "anyio", specifier = "==3.7.0" }]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Re-run with `--offline`. We shouldn't need a network connection to validate an
// already-correct lockfile with immutable metadata.
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}