diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index c873e6a6ac34..202f6ec7cf4e 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -153,6 +153,10 @@ pub enum ErrorKind { #[error("Package `{0}` was not found in the registry.")] 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. #[error("Couldn't parse metadata of {0} from {1}")] MetadataParseError( diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index acd003931915..e1f544836d8c 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -182,6 +182,12 @@ pub struct RegistryClient { timeout: u64, } +#[derive(Debug)] +enum IndexError { + Remote(CachedClientError), + Local(Error), +} + impl RegistryClient { /// Return the [`CachedClient`] used by this client. pub fn cached_client(&self) -> &CachedClient { @@ -229,8 +235,19 @@ impl RegistryClient { 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, + + // The package could not be found in the remote index. ErrorKind::ReqwestError(err) => { if err.status() == Some(StatusCode::NOT_FOUND) || err.status() == Some(StatusCode::UNAUTHORIZED) @@ -240,9 +257,10 @@ impl RegistryClient { } return Err(ErrorKind::from(err).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, package_name: &PackageName, index: &IndexUrl, - ) -> Result, CachedClientError>, Error> { + ) -> Result, IndexError>, Error> { // Format the URL for PyPI. let mut url: Url = index.clone().into(); url.path_segments_mut() @@ -298,7 +316,7 @@ impl RegistryClient { }; if matches!(index, IndexUrl::Path(_)) { - self.fetch_local_index(package_name, &url).await.map(Ok) + self.fetch_local_index(package_name, &url).await } else { self.fetch_remote_index(package_name, &url, &cache_entry, cache_control) .await @@ -312,7 +330,7 @@ impl RegistryClient { url: &Url, cache_entry: &CacheEntry, cache_control: CacheControl, - ) -> Result, CachedClientError>, Error> { + ) -> Result, IndexError>, Error> { let simple_request = self .uncached_client() .get(url.clone()) @@ -367,7 +385,8 @@ impl RegistryClient { cache_control, parse_simple_response, ) - .await; + .await + .map_err(IndexError::Remote); Ok(result) } @@ -377,16 +396,25 @@ impl RegistryClient { &self, package_name: &PackageName, url: &Url, - ) -> Result, Error> { + ) -> Result, IndexError>, Error> { let path = url .to_file_path() .map_err(|()| ErrorKind::NonFileUrl(url.clone()))? .join("index.html"); - let text = fs_err::tokio::read_to_string(&path) - .await - .map_err(ErrorKind::from)?; + let text = match fs_err::tokio::read_to_string(&path).await { + Ok(text) => text, + 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)?; - OwnedArchive::from_unarchived(&metadata) + let metadata = OwnedArchive::from_unarchived(&metadata)?; + Ok(Ok(metadata)) } /// Fetch the metadata for a remote wheel file. diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 57918bfecb8c..52b6c4029999 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -5625,7 +5625,7 @@ fn local_index_absolute() -> Result<()> { -

Links for example-a-961b4c22

+

Links for tqdm

Result<()> { -

Links for example-a-961b4c22

+

Links for tqdm

Result<()> { -

Links for example-a-961b4c22

+

Links for tqdm

Result<()> { -

Links for example-a-961b4c22

+

Links for tqdm

Result<()> { 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#" + + + + + + +

Links for tqdm

+ + + "#, + )?; + + 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(()) +}