mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-25 21:37:51 +00:00
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:
parent
3902bb498d
commit
7fa265a11b
18 changed files with 834 additions and 264 deletions
|
|
@ -143,8 +143,6 @@ impl Display for FileLocation {
|
|||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
rkyv::Archive,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Serialize,
|
||||
|
|
@ -153,6 +151,24 @@ impl Display for FileLocation {
|
|||
#[archive_attr(derive(Debug))]
|
||||
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 {
|
||||
/// Converts a [`UrlString`] to a [`Url`].
|
||||
pub fn to_url(&self) -> Url {
|
||||
|
|
|
|||
|
|
@ -112,8 +112,8 @@ impl FromStr for IndexUrl {
|
|||
type Err = IndexUrlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url = if let Ok(path) = Path::new(s).canonicalize() {
|
||||
VerbatimUrl::from_path(path)?
|
||||
let url = if Path::new(s).exists() {
|
||||
VerbatimUrl::from_absolute_path(std::path::absolute(s)?)?
|
||||
} else {
|
||||
VerbatimUrl::parse_url(s)?
|
||||
};
|
||||
|
|
@ -247,8 +247,8 @@ impl FromStr for FlatIndexLocation {
|
|||
type Err = IndexUrlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url = if let Ok(path) = Path::new(s).canonicalize() {
|
||||
VerbatimUrl::from_path(path)?
|
||||
let url = if Path::new(s).exists() {
|
||||
VerbatimUrl::from_absolute_path(std::path::absolute(s)?)?
|
||||
} else {
|
||||
VerbatimUrl::parse_url(s)?
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ impl UnnamedRequirementUrl for VerbatimUrl {
|
|||
path: impl AsRef<Path>,
|
||||
working_dir: impl AsRef<Path>,
|
||||
) -> 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> {
|
||||
Self::parse_absolute_path(path)
|
||||
Self::from_absolute_path(path)
|
||||
}
|
||||
|
||||
fn parse_unnamed_url(given: impl AsRef<str>) -> Result<Self, Self::Err> {
|
||||
|
|
|
|||
|
|
@ -40,14 +40,29 @@ impl VerbatimUrl {
|
|||
Self { url, given: None }
|
||||
}
|
||||
|
||||
/// Create a [`VerbatimUrl`] from a file path.
|
||||
///
|
||||
/// Assumes that the path is absolute.
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, VerbatimUrlError> {
|
||||
/// 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 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();
|
||||
|
||||
// Normalize the path.
|
||||
let path = normalize_absolute_path(path)
|
||||
// Convert the path to an absolute path, if necessary.
|
||||
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))?;
|
||||
|
||||
// Extract the fragment, if it exists.
|
||||
|
|
@ -65,60 +80,20 @@ impl VerbatimUrl {
|
|||
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.
|
||||
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();
|
||||
|
||||
// Convert the path to an absolute path, if necessary.
|
||||
// Error if the path is relative.
|
||||
let path = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
path
|
||||
} else {
|
||||
return Err(VerbatimUrlError::WorkingDirectory(path.to_path_buf()));
|
||||
};
|
||||
|
||||
// Normalize the path.
|
||||
let path = normalize_absolute_path(&path)
|
||||
.map_err(|err| VerbatimUrlError::Normalization(path.clone(), err))?;
|
||||
let path = normalize_absolute_path(path)
|
||||
.map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?;
|
||||
|
||||
// Extract the fragment, if it exists.
|
||||
let (path, fragment) = split_fragment(&path);
|
||||
|
|
@ -252,14 +227,11 @@ impl Pep508Url for VerbatimUrl {
|
|||
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
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()));
|
||||
}
|
||||
|
||||
Ok(
|
||||
VerbatimUrl::parse_absolute_path(path.as_ref())?
|
||||
.with_given(url.to_string()),
|
||||
)
|
||||
Ok(VerbatimUrl::from_absolute_path(path.as_ref())?.with_given(url.to_string()))
|
||||
}
|
||||
|
||||
// Ex) `https://download.pytorch.org/whl/torch_stable.html`
|
||||
|
|
@ -272,11 +244,11 @@ impl Pep508Url for VerbatimUrl {
|
|||
_ => {
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
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()));
|
||||
}
|
||||
|
||||
Ok(VerbatimUrl::parse_absolute_path(expanded.as_ref())?
|
||||
Ok(VerbatimUrl::from_absolute_path(expanded.as_ref())?
|
||||
.with_given(url.to_string()))
|
||||
}
|
||||
}
|
||||
|
|
@ -284,11 +256,11 @@ impl Pep508Url for VerbatimUrl {
|
|||
// Ex) `../editable/`
|
||||
#[cfg(feature = "non-pep508-extensions")]
|
||||
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()));
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
|
|||
path: impl AsRef<Path>,
|
||||
working_dir: impl AsRef<Path>,
|
||||
) -> 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 is_dir = if let Ok(metadata) = verbatim_path.metadata() {
|
||||
metadata.is_dir()
|
||||
|
|
@ -89,7 +89,7 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl {
|
|||
}
|
||||
|
||||
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 is_dir = if let Ok(metadata) = verbatim_path.metadata() {
|
||||
metadata.is_dir()
|
||||
|
|
|
|||
|
|
@ -507,7 +507,8 @@ impl RequirementSource {
|
|||
ext,
|
||||
url,
|
||||
} => 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,
|
||||
url,
|
||||
}),
|
||||
|
|
@ -516,7 +517,8 @@ impl RequirementSource {
|
|||
editable,
|
||||
url,
|
||||
} => 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,
|
||||
url,
|
||||
}),
|
||||
|
|
@ -744,7 +746,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
// sources in the lockfile, we replace the URL anyway.
|
||||
RequirementSourceWire::Path { path } => {
|
||||
let path = PathBuf::from(path);
|
||||
let url = VerbatimUrl::parse_path(&path, &*CWD)?;
|
||||
let url = VerbatimUrl::from_path(&path, &*CWD)?;
|
||||
Ok(Self::Path {
|
||||
ext: DistExtension::from_path(path.as_path())
|
||||
.map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?,
|
||||
|
|
@ -754,7 +756,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
}
|
||||
RequirementSourceWire::Directory { directory } => {
|
||||
let directory = PathBuf::from(directory);
|
||||
let url = VerbatimUrl::parse_path(&directory, &*CWD)?;
|
||||
let url = VerbatimUrl::from_path(&directory, &*CWD)?;
|
||||
Ok(Self::Directory {
|
||||
install_path: directory,
|
||||
editable: false,
|
||||
|
|
@ -763,7 +765,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
|
|||
}
|
||||
RequirementSourceWire::Editable { editable } => {
|
||||
let editable = PathBuf::from(editable);
|
||||
let url = VerbatimUrl::parse_path(&editable, &*CWD)?;
|
||||
let url = VerbatimUrl::from_path(&editable, &*CWD)?;
|
||||
Ok(Self::Directory {
|
||||
install_path: editable,
|
||||
editable: true,
|
||||
|
|
@ -788,7 +790,7 @@ pub fn redact_git_credentials(url: &mut Url) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pep508_rs::{MarkerTree, VerbatimUrl};
|
||||
|
||||
|
|
@ -823,7 +825,7 @@ mod tests {
|
|||
source: RequirementSource::Directory {
|
||||
install_path: PathBuf::from(path),
|
||||
editable: false,
|
||||
url: VerbatimUrl::from_path(Path::new(path)).unwrap(),
|
||||
url: VerbatimUrl::from_absolute_path(path).unwrap(),
|
||||
},
|
||||
origin: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -484,12 +484,17 @@ fn parse_entry(
|
|||
} else if s.eat_if("-i") || s.eat_if("--index-url") {
|
||||
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
|
||||
let expanded = expand_env_vars(given);
|
||||
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() {
|
||||
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
|
||||
.ok()
|
||||
.filter(|path| path.exists())
|
||||
{
|
||||
VerbatimUrl::from_absolute_path(path).map_err(|err| {
|
||||
RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
}
|
||||
})?
|
||||
} else {
|
||||
VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| {
|
||||
|
|
@ -505,12 +510,17 @@ fn parse_entry(
|
|||
} else if s.eat_if("--extra-index-url") {
|
||||
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
|
||||
let expanded = expand_env_vars(given);
|
||||
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() {
|
||||
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
|
||||
.ok()
|
||||
.filter(|path| path.exists())
|
||||
{
|
||||
VerbatimUrl::from_absolute_path(path).map_err(|err| {
|
||||
RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
}
|
||||
})?
|
||||
} else {
|
||||
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") {
|
||||
let given = parse_value(content, s, |c: char| !['\n', '\r', '#'].contains(&c))?;
|
||||
let expanded = expand_env_vars(given);
|
||||
let url = if let Ok(path) = Path::new(expanded.as_ref()).canonicalize() {
|
||||
VerbatimUrl::from_path(path).map_err(|err| RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
|
||||
.ok()
|
||||
.filter(|path| path.exists())
|
||||
{
|
||||
VerbatimUrl::from_absolute_path(path).map_err(|err| {
|
||||
RequirementsTxtParserError::VerbatimUrl {
|
||||
source: err,
|
||||
url: given.to_string(),
|
||||
start,
|
||||
end: s.cursor(),
|
||||
}
|
||||
})?
|
||||
} else {
|
||||
VerbatimUrl::parse_url(expanded.as_ref()).map_err(|err| {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ impl LoweredRequirement {
|
|||
// relative to workspace: `packages/current_project`
|
||||
// workspace lock root: `../current_workspace`
|
||||
// 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(|()| {
|
||||
LoweringError::RelativeTo(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
|
|
@ -360,7 +360,7 @@ fn path_source(
|
|||
Origin::Project => project_dir,
|
||||
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(|()| {
|
||||
LoweringError::RelativeTo(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
|
|
|
|||
|
|
@ -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/marker.txt` and `lib/python/site-packages` -> `../../marker.txt`
|
||||
/// `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(
|
||||
path: impl AsRef<Path>,
|
||||
base: impl AsRef<Path>,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use distribution_types::{
|
|||
UrlString,
|
||||
};
|
||||
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 pypi_types::{
|
||||
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).
|
||||
let indexes = indexes.map(|locations| {
|
||||
let remotes = indexes.map(|locations| {
|
||||
locations
|
||||
.indexes()
|
||||
.map(IndexUrl::redacted)
|
||||
.chain(locations.flat_index().map(FlatIndexLocation::redacted))
|
||||
.map(UrlString::from)
|
||||
.filter_map(|index_url| match index_url {
|
||||
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
|
||||
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<_>>()
|
||||
});
|
||||
|
||||
|
|
@ -789,16 +832,29 @@ impl Lock {
|
|||
while let Some(package) = queue.pop_front() {
|
||||
// If the lockfile references an index that was not provided, we can't validate it.
|
||||
if let Source::Registry(index) = &package.id.source {
|
||||
if indexes
|
||||
.as_ref()
|
||||
.is_some_and(|indexes| !indexes.contains(index))
|
||||
{
|
||||
return Ok(SatisfiesResult::MissingIndex(
|
||||
&package.id.name,
|
||||
&package.id.version,
|
||||
index,
|
||||
));
|
||||
}
|
||||
match index {
|
||||
RegistrySource::Url(url) => {
|
||||
if remotes
|
||||
.as_ref()
|
||||
.is_some_and(|remotes| !remotes.contains(url))
|
||||
{
|
||||
return Ok(SatisfiesResult::MissingRemoteIndex(
|
||||
&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).
|
||||
|
|
@ -935,8 +991,10 @@ pub enum SatisfiesResult<'lock> {
|
|||
MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
|
||||
/// The lockfile is missing a workspace member.
|
||||
MissingRoot(PackageName),
|
||||
/// The lockfile referenced an index that was not provided
|
||||
MissingIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
|
||||
/// The lockfile referenced a remote index that was not provided
|
||||
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.
|
||||
MissingMetadata(&'lock PackageName, &'lock Version),
|
||||
/// 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> {
|
||||
if let Some(best_wheel_index) = self.find_best_wheel(tags) {
|
||||
return match &self.id.source {
|
||||
Source::Registry(url) => {
|
||||
Source::Registry(source) => {
|
||||
let wheels = self
|
||||
.wheels
|
||||
.iter()
|
||||
.map(|wheel| wheel.to_registry_dist(url.to_url()))
|
||||
.map(|wheel| wheel.to_registry_dist(source, workspace_root))
|
||||
.collect::<Result<_, LockError>>()?;
|
||||
let reg_built_dist = RegistryBuiltDist {
|
||||
wheels,
|
||||
|
|
@ -1295,7 +1353,7 @@ impl Package {
|
|||
}
|
||||
.into()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sdist) = self.to_source_dist(workspace_root)? {
|
||||
return Ok(Dist::Source(sdist));
|
||||
|
|
@ -1388,7 +1446,7 @@ impl Package {
|
|||
};
|
||||
distribution_types::SourceDist::DirectUrl(direct_dist)
|
||||
}
|
||||
Source::Registry(url) => {
|
||||
Source::Registry(RegistrySource::Url(url)) => {
|
||||
let Some(ref sdist) = self.sdist else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
@ -1418,6 +1476,51 @@ impl Package {
|
|||
});
|
||||
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 {
|
||||
name: self.id.name.clone(),
|
||||
version: self.id.version.clone(),
|
||||
|
|
@ -1597,14 +1700,6 @@ impl Package {
|
|||
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`].
|
||||
fn hashes(&self) -> Vec<HashDigest> {
|
||||
let mut hashes = Vec::new();
|
||||
|
|
@ -1636,7 +1731,7 @@ impl Package {
|
|||
|
||||
/// Attempts to construct a `VerbatimUrl` from the given `Path`.
|
||||
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(),
|
||||
err,
|
||||
})?;
|
||||
|
|
@ -1836,11 +1931,17 @@ impl From<PackageId> for PackageIdForDependency {
|
|||
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
|
||||
#[serde(try_from = "SourceWire")]
|
||||
enum Source {
|
||||
Registry(UrlString),
|
||||
/// A registry or `--find-links` index.
|
||||
Registry(RegistrySource),
|
||||
/// A Git repository.
|
||||
Git(UrlString, GitSource),
|
||||
/// A direct HTTP(S) URL.
|
||||
Direct(UrlString, DirectSource),
|
||||
/// A path to a local source or built archive.
|
||||
Path(PathBuf),
|
||||
/// A path to a local directory.
|
||||
Directory(PathBuf),
|
||||
/// A path to a local directory that should be installed as editable.
|
||||
Editable(PathBuf),
|
||||
}
|
||||
|
||||
|
|
@ -1862,7 +1963,7 @@ impl Source {
|
|||
|
||||
fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Source, LockError> {
|
||||
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) => {
|
||||
Ok(Source::from_direct_built_dist(direct_dist))
|
||||
}
|
||||
|
|
@ -1876,7 +1977,7 @@ impl Source {
|
|||
) -> Result<Source, LockError> {
|
||||
match *source_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) => {
|
||||
Ok(Source::from_direct_source_dist(direct_dist))
|
||||
|
|
@ -1893,12 +1994,18 @@ impl Source {
|
|||
}
|
||||
}
|
||||
|
||||
fn from_registry_built_dist(reg_dist: &RegistryBuiltDist) -> Source {
|
||||
Source::from_index_url(®_dist.best_wheel().index)
|
||||
fn from_registry_built_dist(
|
||||
reg_dist: &RegistryBuiltDist,
|
||||
root: &Path,
|
||||
) -> Result<Source, LockError> {
|
||||
Source::from_index_url(®_dist.best_wheel().index, root)
|
||||
}
|
||||
|
||||
fn from_registry_source_dist(reg_dist: &RegistrySourceDist) -> Source {
|
||||
Source::from_index_url(®_dist.index)
|
||||
fn from_registry_source_dist(
|
||||
reg_dist: &RegistrySourceDist,
|
||||
root: &Path,
|
||||
) -> Result<Source, LockError> {
|
||||
Source::from_index_url(®_dist.index, root)
|
||||
}
|
||||
|
||||
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> {
|
||||
let path = relative_to(&path_dist.install_path, root)
|
||||
.or_else(|_| std::path::absolute(&path_dist.install_path))
|
||||
.map_err(LockErrorKind::DistributionRelativePath)?;
|
||||
Ok(Source::Path(path))
|
||||
}
|
||||
|
||||
fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Source, LockError> {
|
||||
let path = relative_to(&path_dist.install_path, root)
|
||||
.or_else(|_| std::path::absolute(&path_dist.install_path))
|
||||
.map_err(LockErrorKind::DistributionRelativePath)?;
|
||||
|
||||
Ok(Source::Path(path))
|
||||
}
|
||||
|
||||
|
|
@ -1939,6 +2047,7 @@ impl Source {
|
|||
root: &Path,
|
||||
) -> Result<Source, LockError> {
|
||||
let path = relative_to(&directory_dist.install_path, root)
|
||||
.or_else(|_| std::path::absolute(&directory_dist.install_path))
|
||||
.map_err(LockErrorKind::DistributionRelativePath)?;
|
||||
if directory_dist.editable {
|
||||
Ok(Source::Editable(path))
|
||||
|
|
@ -1947,10 +2056,23 @@ impl Source {
|
|||
}
|
||||
}
|
||||
|
||||
fn from_index_url(index_url: &IndexUrl) -> Source {
|
||||
// Remove any sensitive credentials from the index URL.
|
||||
let redacted = index_url.redacted();
|
||||
Source::Registry(UrlString::from(redacted.as_ref()))
|
||||
fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Source, LockError> {
|
||||
match index_url {
|
||||
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
|
||||
// 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 {
|
||||
|
|
@ -1983,9 +2105,17 @@ impl Source {
|
|||
fn to_toml(&self, table: &mut Table) {
|
||||
let mut source_table = InlineTable::new();
|
||||
match *self {
|
||||
Source::Registry(ref url) => {
|
||||
source_table.insert("registry", Value::from(url.as_ref()));
|
||||
}
|
||||
Source::Registry(ref source) => match source {
|
||||
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_table.insert("git", Value::from(url.as_ref()));
|
||||
}
|
||||
|
|
@ -2018,10 +2148,15 @@ impl Source {
|
|||
impl std::fmt::Display for Source {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -2060,7 +2195,7 @@ impl Source {
|
|||
#[serde(untagged)]
|
||||
enum SourceWire {
|
||||
Registry {
|
||||
registry: UrlString,
|
||||
registry: RegistrySource,
|
||||
},
|
||||
Git {
|
||||
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)]
|
||||
struct DirectSource {
|
||||
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> {
|
||||
match &self {
|
||||
SourceDist::Url { metadata, .. } => metadata.hash.as_ref(),
|
||||
|
|
@ -2304,15 +2504,38 @@ impl SourceDist {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let url = normalize_file_location(®_dist.file.url)
|
||||
.map_err(LockErrorKind::InvalidFileUrl)
|
||||
.map_err(LockError::from)?;
|
||||
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
|
||||
let size = reg_dist.file.size;
|
||||
Ok(Some(SourceDist::Url {
|
||||
url,
|
||||
metadata: SourceDistMetadata { hash, size },
|
||||
}))
|
||||
match ®_dist.index {
|
||||
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
|
||||
let url = normalize_file_location(®_dist.file.url)
|
||||
.map_err(LockErrorKind::InvalidFileUrl)
|
||||
.map_err(LockError::from)?;
|
||||
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
|
||||
let size = reg_dist.file.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(®_dist_path, index_path)
|
||||
.or_else(|_| std::path::absolute(®_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(
|
||||
|
|
@ -2558,17 +2781,40 @@ impl Wheel {
|
|||
|
||||
fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Wheel, LockError> {
|
||||
let filename = wheel.filename.clone();
|
||||
let url = normalize_file_location(&wheel.file.url)
|
||||
.map_err(LockErrorKind::InvalidFileUrl)
|
||||
.map_err(LockError::from)?;
|
||||
let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
|
||||
let size = wheel.file.size;
|
||||
Ok(Wheel {
|
||||
url: WheelWireSource::Url { url },
|
||||
hash,
|
||||
size,
|
||||
filename,
|
||||
})
|
||||
match &wheel.index {
|
||||
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
|
||||
let url = normalize_file_location(&wheel.file.url)
|
||||
.map_err(LockErrorKind::InvalidFileUrl)
|
||||
.map_err(LockError::from)?;
|
||||
let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
|
||||
let size = wheel.file.size;
|
||||
Ok(Wheel {
|
||||
url: WheelWireSource::Url { url },
|
||||
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 {
|
||||
|
|
@ -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 url_string = match &self.url {
|
||||
WheelWireSource::Url { url } => url.clone(),
|
||||
WheelWireSource::Filename { .. } => {
|
||||
return Err(LockErrorKind::MissingUrl {
|
||||
name: self.filename.name.clone(),
|
||||
version: self.filename.version.clone(),
|
||||
}
|
||||
.into())
|
||||
|
||||
match source {
|
||||
RegistrySource::Url(index_url) => {
|
||||
let file_url = match &self.url {
|
||||
WheelWireSource::Url { url } => url,
|
||||
WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
|
||||
return Err(LockErrorKind::MissingUrl {
|
||||
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,
|
||||
})
|
||||
}
|
||||
};
|
||||
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(url_string),
|
||||
yanked: None,
|
||||
});
|
||||
let index = IndexUrl::Url(VerbatimUrl::from_url(url));
|
||||
Ok(RegistryBuiltWheel {
|
||||
filename,
|
||||
file,
|
||||
index,
|
||||
})
|
||||
RegistrySource::Path(index_path) => {
|
||||
let file_path = match &self.url {
|
||||
WheelWireSource::Path { path } => path,
|
||||
WheelWireSource::Url { .. } | WheelWireSource::Filename { .. } => {
|
||||
return Err(LockErrorKind::MissingPath {
|
||||
name: filename.name,
|
||||
version: filename.version,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let file_url = Url::from_file_path(root.join(index_path).join(file_path))
|
||||
.map_err(|()| LockErrorKind::PathToUrl)?;
|
||||
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(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)]
|
||||
#[serde(untagged)]
|
||||
enum WheelWireSource {
|
||||
/// Used for all wheels except path wheels.
|
||||
/// Used for all wheels that come from remote sources.
|
||||
Url {
|
||||
/// A URL or file path (via `file://`) where the wheel 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 wheel file originally came from.
|
||||
/// A URL where the wheel 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 wheel file
|
||||
/// originally came from.
|
||||
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.
|
||||
///
|
||||
/// 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 } => {
|
||||
table.insert("url", Value::from(url.as_ref()));
|
||||
}
|
||||
WheelWireSource::Path { path } => {
|
||||
table.insert("path", Value::from(PortablePath::from(path).to_string()));
|
||||
}
|
||||
WheelWireSource::Filename { filename } => {
|
||||
table.insert("filename", Value::from(filename.to_string()));
|
||||
}
|
||||
|
|
@ -2687,7 +2983,6 @@ impl TryFrom<WheelWire> for Wheel {
|
|||
type Error = String;
|
||||
|
||||
fn try_from(wire: WheelWire) -> Result<Wheel, String> {
|
||||
// If necessary, extract the filename from the URL.
|
||||
let filename = match &wire.url {
|
||||
WheelWireSource::Url { url } => {
|
||||
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}")
|
||||
})?
|
||||
}
|
||||
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(),
|
||||
};
|
||||
|
||||
|
|
@ -2914,7 +3220,7 @@ fn normalize_requirement(
|
|||
url: _,
|
||||
} => {
|
||||
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)?;
|
||||
|
||||
Ok(Requirement {
|
||||
|
|
@ -2935,7 +3241,7 @@ fn normalize_requirement(
|
|||
url: _,
|
||||
} => {
|
||||
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)?;
|
||||
|
||||
Ok(Requirement {
|
||||
|
|
@ -3098,8 +3404,8 @@ enum LockErrorKind {
|
|||
/// The kind of the invalid source.
|
||||
source_type: &'static str,
|
||||
},
|
||||
/// An error that occurs when a distribution indicates that it is sourced from a registry, but
|
||||
/// is missing a URL.
|
||||
/// An error that occurs when a distribution indicates that it is sourced from a remote
|
||||
/// registry, but is missing a URL.
|
||||
#[error("found registry distribution {name}=={version} without a valid URL")]
|
||||
MissingUrl {
|
||||
/// 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.
|
||||
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
|
||||
/// is missing a filename.
|
||||
#[error("found registry distribution {id} without a valid filename")]
|
||||
|
|
@ -3137,6 +3452,13 @@ enum LockErrorKind {
|
|||
#[source]
|
||||
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
|
||||
/// missing a `version` field.
|
||||
#[error(
|
||||
|
|
@ -3171,6 +3493,19 @@ enum LockErrorKind {
|
|||
#[source]
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Ok(
|
|||
),
|
||||
version: "4.3.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -71,8 +73,10 @@ Ok(
|
|||
),
|
||||
version: "4.3.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Ok(
|
|||
),
|
||||
version: "4.3.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -78,8 +80,10 @@ Ok(
|
|||
),
|
||||
version: "4.3.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -63,8 +65,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -98,8 +102,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -122,8 +128,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
|
|
@ -133,8 +141,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 1,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -63,8 +65,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -98,8 +102,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -122,8 +128,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
|
|
@ -133,8 +141,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 1,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -63,8 +65,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -98,8 +102,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -122,8 +128,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 0,
|
||||
|
|
@ -133,8 +141,10 @@ Ok(
|
|||
),
|
||||
version: "0.1.0",
|
||||
source: Registry(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
Url(
|
||||
UrlString(
|
||||
"https://pypi.org/simple",
|
||||
),
|
||||
),
|
||||
),
|
||||
}: 1,
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ impl Workspace {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let url = VerbatimUrl::from_path(&member.root)
|
||||
let url = VerbatimUrl::from_absolute_path(&member.root)
|
||||
.expect("path is valid URL")
|
||||
.with_given(member.root.to_string_lossy());
|
||||
Some(Requirement {
|
||||
|
|
|
|||
|
|
@ -723,9 +723,15 @@ impl ValidatedLock {
|
|||
debug!("Ignoring existing lockfile due to missing root package: `{name}`");
|
||||
Ok(Self::Preferable(lock))
|
||||
}
|
||||
SatisfiesResult::MissingIndex(name, version, index) => {
|
||||
SatisfiesResult::MissingRemoteIndex(name, version, index) => {
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6493,7 +6493,28 @@ fn lock_warn_missing_transitive_lower_bounds() -> Result<()> {
|
|||
fn lock_find_links_local_wheel() -> Result<()> {
|
||||
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#"
|
||||
[project]
|
||||
name = "project"
|
||||
|
|
@ -6504,19 +6525,20 @@ fn lock_find_links_local_wheel() -> Result<()> {
|
|||
[tool.uv]
|
||||
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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
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!({
|
||||
filters => context.filters(),
|
||||
|
|
@ -6543,34 +6565,37 @@ fn lock_find_links_local_wheel() -> Result<()> {
|
|||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "1000.0.0"
|
||||
source = { registry = "file://[WORKSPACE]/scripts/links" }
|
||||
source = { registry = "../links" }
|
||||
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`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
// 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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtualenv at: .venv
|
||||
Prepared 1 package 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
|
||||
"###);
|
||||
|
||||
|
|
@ -6582,7 +6607,28 @@ fn lock_find_links_local_wheel() -> Result<()> {
|
|||
fn lock_find_links_local_sdist() -> Result<()> {
|
||||
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#"
|
||||
[project]
|
||||
name = "project"
|
||||
|
|
@ -6593,19 +6639,20 @@ fn lock_find_links_local_sdist() -> Result<()> {
|
|||
[tool.uv]
|
||||
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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
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!({
|
||||
filters => context.filters(),
|
||||
|
|
@ -6632,32 +6679,35 @@ fn lock_find_links_local_sdist() -> Result<()> {
|
|||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "999.0.0"
|
||||
source = { registry = "file://[WORKSPACE]/scripts/links" }
|
||||
sdist = { url = "file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz" }
|
||||
source = { registry = "../links" }
|
||||
sdist = { path = "tqdm-999.0.0.tar.gz" }
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// 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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
// 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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Creating virtualenv at: .venv
|
||||
Prepared 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
|
||||
"###);
|
||||
|
||||
|
|
@ -6857,6 +6907,14 @@ fn lock_local_index() -> Result<()> {
|
|||
let tqdm = root.child("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");
|
||||
index.write_str(&formatdoc! {r#"
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -6867,14 +6925,14 @@ fn lock_local_index() -> Result<()> {
|
|||
<body>
|
||||
<h1>Links for tqdm</h1>
|
||||
<a
|
||||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||||
href="{}"
|
||||
data-requires-python=">=3.8"
|
||||
>
|
||||
tqdm-1000.0.0-py3-none-any.whl
|
||||
</a>
|
||||
</body>
|
||||
</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");
|
||||
|
||||
|
|
@ -6931,9 +6989,9 @@ fn lock_local_index() -> Result<()> {
|
|||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "1000.0.0"
|
||||
source = { registry = "file://[TMP]" }
|
||||
source = { registry = "../../[TMP]/simple-html" }
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue