mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Gracefully handle non-existent packages in local indexes (#4545)
## Summary Ensures that local indexes can be used as `--extra-index-url` by gracefully handling "404" errors. Closes https://github.com/astral-sh/uv/issues/4540.
This commit is contained in:
parent
d7f195fdc9
commit
a5b5856521
3 changed files with 93 additions and 15 deletions
|
@ -153,6 +153,10 @@ pub enum ErrorKind {
|
||||||
#[error("Package `{0}` was not found in the registry.")]
|
#[error("Package `{0}` was not found in the registry.")]
|
||||||
PackageNotFound(String),
|
PackageNotFound(String),
|
||||||
|
|
||||||
|
/// The package was not found in the local (file-based) index.
|
||||||
|
#[error("Package `{0}` was not found in the local index.")]
|
||||||
|
FileNotFound(String),
|
||||||
|
|
||||||
/// The metadata file could not be parsed.
|
/// The metadata file could not be parsed.
|
||||||
#[error("Couldn't parse metadata of {0} from {1}")]
|
#[error("Couldn't parse metadata of {0} from {1}")]
|
||||||
MetadataParseError(
|
MetadataParseError(
|
||||||
|
|
|
@ -182,6 +182,12 @@ pub struct RegistryClient {
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum IndexError {
|
||||||
|
Remote(CachedClientError<Error>),
|
||||||
|
Local(Error),
|
||||||
|
}
|
||||||
|
|
||||||
impl RegistryClient {
|
impl RegistryClient {
|
||||||
/// Return the [`CachedClient`] used by this client.
|
/// Return the [`CachedClient`] used by this client.
|
||||||
pub fn cached_client(&self) -> &CachedClient {
|
pub fn cached_client(&self) -> &CachedClient {
|
||||||
|
@ -229,8 +235,19 @@ impl RegistryClient {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(CachedClientError::Client(err)) => match err.into_kind() {
|
Err(IndexError::Local(err)) => {
|
||||||
|
match err.into_kind() {
|
||||||
|
// The package could not be found in the local index.
|
||||||
|
ErrorKind::FileNotFound(_) => continue,
|
||||||
|
|
||||||
|
other => return Err(other.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(IndexError::Remote(CachedClientError::Client(err))) => match err.into_kind() {
|
||||||
|
// The package is unavailable due to a lack of connectivity.
|
||||||
ErrorKind::Offline(_) => continue,
|
ErrorKind::Offline(_) => continue,
|
||||||
|
|
||||||
|
// The package could not be found in the remote index.
|
||||||
ErrorKind::ReqwestError(err) => {
|
ErrorKind::ReqwestError(err) => {
|
||||||
if err.status() == Some(StatusCode::NOT_FOUND)
|
if err.status() == Some(StatusCode::NOT_FOUND)
|
||||||
|| err.status() == Some(StatusCode::UNAUTHORIZED)
|
|| err.status() == Some(StatusCode::UNAUTHORIZED)
|
||||||
|
@ -240,9 +257,10 @@ impl RegistryClient {
|
||||||
}
|
}
|
||||||
return Err(ErrorKind::from(err).into());
|
return Err(ErrorKind::from(err).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
other => return Err(other.into()),
|
other => return Err(other.into()),
|
||||||
},
|
},
|
||||||
Err(CachedClientError::Callback(err)) => return Err(err),
|
Err(IndexError::Remote(CachedClientError::Callback(err))) => return Err(err),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +284,7 @@ impl RegistryClient {
|
||||||
&self,
|
&self,
|
||||||
package_name: &PackageName,
|
package_name: &PackageName,
|
||||||
index: &IndexUrl,
|
index: &IndexUrl,
|
||||||
) -> Result<Result<OwnedArchive<SimpleMetadata>, CachedClientError<Error>>, Error> {
|
) -> Result<Result<OwnedArchive<SimpleMetadata>, IndexError>, Error> {
|
||||||
// Format the URL for PyPI.
|
// Format the URL for PyPI.
|
||||||
let mut url: Url = index.clone().into();
|
let mut url: Url = index.clone().into();
|
||||||
url.path_segments_mut()
|
url.path_segments_mut()
|
||||||
|
@ -298,7 +316,7 @@ impl RegistryClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches!(index, IndexUrl::Path(_)) {
|
if matches!(index, IndexUrl::Path(_)) {
|
||||||
self.fetch_local_index(package_name, &url).await.map(Ok)
|
self.fetch_local_index(package_name, &url).await
|
||||||
} else {
|
} else {
|
||||||
self.fetch_remote_index(package_name, &url, &cache_entry, cache_control)
|
self.fetch_remote_index(package_name, &url, &cache_entry, cache_control)
|
||||||
.await
|
.await
|
||||||
|
@ -312,7 +330,7 @@ impl RegistryClient {
|
||||||
url: &Url,
|
url: &Url,
|
||||||
cache_entry: &CacheEntry,
|
cache_entry: &CacheEntry,
|
||||||
cache_control: CacheControl,
|
cache_control: CacheControl,
|
||||||
) -> Result<Result<OwnedArchive<SimpleMetadata>, CachedClientError<Error>>, Error> {
|
) -> Result<Result<OwnedArchive<SimpleMetadata>, IndexError>, Error> {
|
||||||
let simple_request = self
|
let simple_request = self
|
||||||
.uncached_client()
|
.uncached_client()
|
||||||
.get(url.clone())
|
.get(url.clone())
|
||||||
|
@ -367,7 +385,8 @@ impl RegistryClient {
|
||||||
cache_control,
|
cache_control,
|
||||||
parse_simple_response,
|
parse_simple_response,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.map_err(IndexError::Remote);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,16 +396,25 @@ impl RegistryClient {
|
||||||
&self,
|
&self,
|
||||||
package_name: &PackageName,
|
package_name: &PackageName,
|
||||||
url: &Url,
|
url: &Url,
|
||||||
) -> Result<OwnedArchive<SimpleMetadata>, Error> {
|
) -> Result<Result<OwnedArchive<SimpleMetadata>, IndexError>, Error> {
|
||||||
let path = url
|
let path = url
|
||||||
.to_file_path()
|
.to_file_path()
|
||||||
.map_err(|()| ErrorKind::NonFileUrl(url.clone()))?
|
.map_err(|()| ErrorKind::NonFileUrl(url.clone()))?
|
||||||
.join("index.html");
|
.join("index.html");
|
||||||
let text = fs_err::tokio::read_to_string(&path)
|
let text = match fs_err::tokio::read_to_string(&path).await {
|
||||||
.await
|
Ok(text) => text,
|
||||||
.map_err(ErrorKind::from)?;
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
return Ok(Err(IndexError::Local(Error::from(
|
||||||
|
ErrorKind::FileNotFound(package_name.to_string()),
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Error::from(ErrorKind::Io(err)));
|
||||||
|
}
|
||||||
|
};
|
||||||
let metadata = SimpleMetadata::from_html(&text, package_name, url)?;
|
let metadata = SimpleMetadata::from_html(&text, package_name, url)?;
|
||||||
OwnedArchive::from_unarchived(&metadata)
|
let metadata = OwnedArchive::from_unarchived(&metadata)?;
|
||||||
|
Ok(Ok(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the metadata for a remote wheel file.
|
/// Fetch the metadata for a remote wheel file.
|
||||||
|
|
|
@ -5625,7 +5625,7 @@ fn local_index_absolute() -> Result<()> {
|
||||||
<meta name="pypi:repository-version" content="1.1" />
|
<meta name="pypi:repository-version" content="1.1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Links for example-a-961b4c22</h1>
|
<h1>Links for tqdm</h1>
|
||||||
<a
|
<a
|
||||||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||||||
data-requires-python=">=3.8"
|
data-requires-python=">=3.8"
|
||||||
|
@ -5676,7 +5676,7 @@ fn local_index_relative() -> Result<()> {
|
||||||
<meta name="pypi:repository-version" content="1.1" />
|
<meta name="pypi:repository-version" content="1.1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Links for example-a-961b4c22</h1>
|
<h1>Links for tqdm</h1>
|
||||||
<a
|
<a
|
||||||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||||||
data-requires-python=">=3.8"
|
data-requires-python=">=3.8"
|
||||||
|
@ -5727,7 +5727,7 @@ fn local_index_requirements_txt_absolute() -> Result<()> {
|
||||||
<meta name="pypi:repository-version" content="1.1" />
|
<meta name="pypi:repository-version" content="1.1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Links for example-a-961b4c22</h1>
|
<h1>Links for tqdm</h1>
|
||||||
<a
|
<a
|
||||||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||||||
data-requires-python=">=3.8"
|
data-requires-python=">=3.8"
|
||||||
|
@ -5783,7 +5783,7 @@ fn local_index_requirements_txt_relative() -> Result<()> {
|
||||||
<meta name="pypi:repository-version" content="1.1" />
|
<meta name="pypi:repository-version" content="1.1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Links for example-a-961b4c22</h1>
|
<h1>Links for tqdm</h1>
|
||||||
<a
|
<a
|
||||||
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
href="{}/tqdm-1000.0.0-py3-none-any.whl"
|
||||||
data-requires-python=">=3.8"
|
data-requires-python=">=3.8"
|
||||||
|
@ -5820,3 +5820,49 @@ fn local_index_requirements_txt_relative() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve against a local directory laid out as a PEP 503-compatible index, falling back to
|
||||||
|
/// the default index.
|
||||||
|
#[test]
|
||||||
|
fn local_index_fallback() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let root = context.temp_dir.child("simple-html");
|
||||||
|
fs_err::create_dir_all(&root)?;
|
||||||
|
|
||||||
|
let tqdm = root.child("tqdm");
|
||||||
|
fs_err::create_dir_all(&tqdm)?;
|
||||||
|
|
||||||
|
let index = tqdm.child("index.html");
|
||||||
|
index.write_str(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="pypi:repository-version" content="1.1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Links for tqdm</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.pip_install()
|
||||||
|
.arg("iniconfig")
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg(Url::from_directory_path(root).unwrap().as_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Prepared 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue