Skip to content

Commit

Permalink
Merge branch 'main' into zb/trace-is_extended_transient_error
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 19, 2024
2 parents 6914432 + 9fb7f81 commit 950a81a
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 21 deletions.
59 changes: 57 additions & 2 deletions crates/uv-build-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,6 @@ pub fn build_wheel(
entry.file_type(),
));
}

entry.path();
}

// Add the license files
Expand Down Expand Up @@ -416,6 +414,63 @@ pub fn build_wheel(
Ok(filename)
}

/// Build a wheel from the source tree and place it in the output directory.
pub fn build_editable(
source_tree: &Path,
wheel_dir: &Path,
metadata_directory: Option<&Path>,
uv_version: &str,
) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
pyproject_toml.check_build_system("1.0.0+test");

check_metadata_directory(source_tree, metadata_directory, &pyproject_toml)?;

let filename = WheelFilename {
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
build_tag: None,
python_tag: vec!["py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
};

let wheel_path = wheel_dir.join(filename.to_string());
debug!("Writing wheel at {}", wheel_path.user_display());
let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?);

debug!("Adding pth file to {}", wheel_path.user_display());
let module_root = pyproject_toml
.wheel_settings()
.and_then(|wheel_settings| wheel_settings.module_root.as_deref())
.unwrap_or_else(|| Path::new("src"));
if module_root.is_absolute() {
return Err(Error::AbsoluteModuleRoot(module_root.to_path_buf()));
}
let src_root = source_tree.join(module_root);
let module_root = src_root.join(pyproject_toml.name().as_dist_info_name().as_ref());
if !module_root.join("__init__.py").is_file() {
return Err(Error::MissingModule(module_root));
}
wheel_writer.write_bytes(
&format!("{}.pth", pyproject_toml.name().as_dist_info_name()),
src_root.as_os_str().as_encoded_bytes(),
)?;

debug!("Adding metadata files to: `{}`", wheel_path.user_display());
let dist_info_dir = write_dist_info(
&mut wheel_writer,
&pyproject_toml,
&filename,
source_tree,
uv_version,
)?;
wheel_writer.close(&dist_info_dir)?;

Ok(filename)
}

/// Add the files and directories matching from the source tree matching any of the globs in the
/// wheel subdirectory.
fn wheel_subdir_from_globs(
Expand Down
17 changes: 16 additions & 1 deletion crates/uv-client/src/base_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use uv_warnings::warn_user_once;
use crate::linehaul::LineHaul;
use crate::middleware::OfflineMiddleware;
use crate::tls::read_identity;
use crate::Connectivity;
use crate::{Connectivity, WrappedReqwestError};

pub const DEFAULT_RETRIES: u32 = 3;

Expand Down Expand Up @@ -461,6 +461,20 @@ impl RetryableStrategy for UvRetryableStrategy {
///
/// These cases should be safe to retry with [`Retryable::Transient`].
pub(crate) fn is_extended_transient_error(err: &dyn Error) -> bool {
// First, look for `WrappedReqwestError`, which wraps `reqwest::Error` but doesn't always
// include it in the source.
if let Some(err) = find_source::<WrappedReqwestError>(&err) {
if let Some(io) = find_source::<std::io::Error>(&err) {
if io.kind() == std::io::ErrorKind::ConnectionReset
|| io.kind() == std::io::ErrorKind::UnexpectedEof
{
return true;
}
}
}

// Next, look for `reqwest_middleware::Error`, which wraps `reqwest::Error`, but also includes
// errors from the middleware stack.
if let Some(err) = find_source::<reqwest_middleware::Error>(&err) {
if let Some(io) = find_source::<std::io::Error>(&err) {
if io.kind() == std::io::ErrorKind::ConnectionReset
Expand All @@ -474,6 +488,7 @@ pub(crate) fn is_extended_transient_error(err: &dyn Error) -> bool {
}
}

// Finally, look for `reqwest::Error`, which is the most common error type.
if let Some(err) = find_source::<reqwest::Error>(&err) {
if let Some(io) = find_source::<std::io::Error>(&err) {
if io.kind() == std::io::ErrorKind::ConnectionReset
Expand Down
9 changes: 5 additions & 4 deletions crates/uv-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,10 @@ impl Deref for WrappedReqwestError {
impl Display for WrappedReqwestError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_likely_offline() {
// Insert an extra hint, we'll show the wrapped error through `source`
f.write_str("Could not connect, are you offline?")
} else {
// Show the wrapped error
Display::fmt(&self.0, f)
}
}
Expand All @@ -332,11 +334,10 @@ impl Display for WrappedReqwestError {
impl std::error::Error for WrappedReqwestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if self.is_likely_offline() {
match &self.0 {
reqwest_middleware::Error::Middleware(err) => Some(err.as_ref()),
reqwest_middleware::Error::Reqwest(err) => Some(err),
}
// `Display` is inserting an extra message, so we need to show the wrapped error
Some(&self.0)
} else {
// `Display` is showing the wrapped error, continue with its source
self.0.source()
}
}
Expand Down
53 changes: 41 additions & 12 deletions crates/uv/src/commands/build_backend.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
#![allow(clippy::print_stdout)]

use crate::commands::ExitStatus;
use anyhow::Result;
use anyhow::{Context, Result};
use std::env;
use std::io::Write;
use std::path::Path;
use uv_build_backend::SourceDistSettings;

/// PEP 517 hook to build a source distribution.
pub(crate) fn build_sdist(sdist_directory: &Path) -> Result<ExitStatus> {
let filename = uv_build_backend::build_source_dist(
&env::current_dir()?,
sdist_directory,
SourceDistSettings::default(),
uv_version::version(),
)?;
println!("{filename}");
// Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
Ok(ExitStatus::Success)
}

/// PEP 517 hook to build a wheel.
pub(crate) fn build_wheel(
wheel_directory: &Path,
metadata_directory: Option<&Path>,
Expand All @@ -26,38 +31,62 @@ pub(crate) fn build_wheel(
metadata_directory,
uv_version::version(),
)?;
println!("{filename}");
// Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
Ok(ExitStatus::Success)
}

/// PEP 660 hook to build a wheel.
pub(crate) fn build_editable(
_wheel_directory: &Path,
_metadata_directory: Option<&Path>,
wheel_directory: &Path,
metadata_directory: Option<&Path>,
) -> Result<ExitStatus> {
todo!()
let filename = uv_build_backend::build_editable(
&env::current_dir()?,
wheel_directory,
metadata_directory,
uv_version::version(),
)?;
// Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
Ok(ExitStatus::Success)
}

/// Not used from Python code, exists for symmetry with PEP 517.
pub(crate) fn get_requires_for_build_sdist() -> Result<ExitStatus> {
todo!()
unimplemented!("uv does not support extra requires")
}

/// Not used from Python code, exists for symmetry with PEP 517.
pub(crate) fn get_requires_for_build_wheel() -> Result<ExitStatus> {
todo!()
unimplemented!("uv does not support extra requires")
}

/// PEP 517 hook to just emit metadata through `.dist-info`.
pub(crate) fn prepare_metadata_for_build_wheel(metadata_directory: &Path) -> Result<ExitStatus> {
let filename = uv_build_backend::metadata(
&env::current_dir()?,
metadata_directory,
uv_version::version(),
)?;
println!("{filename}");
// Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
Ok(ExitStatus::Success)
}

/// Not used from Python code, exists for symmetry with PEP 660.
pub(crate) fn get_requires_for_build_editable() -> Result<ExitStatus> {
todo!()
unimplemented!("uv does not support extra requires")
}

pub(crate) fn prepare_metadata_for_build_editable(_wheel_directory: &Path) -> Result<ExitStatus> {
todo!()
/// PEP 660 hook to just emit metadata through `.dist-info`.
pub(crate) fn prepare_metadata_for_build_editable(metadata_directory: &Path) -> Result<ExitStatus> {
let filename = uv_build_backend::metadata(
&env::current_dir()?,
metadata_directory,
uv_version::version(),
)?;
// Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
Ok(ExitStatus::Success)
}
63 changes: 63 additions & 0 deletions crates/uv/tests/it/build_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use indoc::indoc;
use std::env;
use std::io::BufReader;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use uv_static::EnvVars;

Expand Down Expand Up @@ -142,3 +143,65 @@ fn built_by_uv_direct() -> Result<()> {

Ok(())
}

/// Test that editables work.
///
/// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel,
/// so we call the build backend directly.
#[test]
fn built_by_uv_editable() -> Result<()> {
let context = TestContext::new("3.12");
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");

// Without the editable, pytest fails.
context.pip_install().arg("pytest").assert().success();
Command::new(context.interpreter())
.arg("-m")
.arg("pytest")
.current_dir(built_by_uv)
.assert()
.failure();

// Build and install the editable. Normally, this should be one step with the editable never
// been seen, but we have to split it for the test.
let wheel_dir = TempDir::new()?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(wheel_dir.path())
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
built_by_uv-0.1.0-py3-none-any.whl
----- stderr -----
"###);
context
.pip_install()
.arg(wheel_dir.path().join("built_by_uv-0.1.0-py3-none-any.whl"))
.assert()
.success();

drop(wheel_dir);

// Now, pytest passes.
uv_snapshot!(Command::new(context.interpreter())
.arg("-m")
.arg("pytest")
// Avoid showing absolute paths
.arg("--no-header")
// Otherwise, the header has a different length on windows
.arg("--quiet")
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
.. [100%]
2 passed in [TIME]
----- stderr -----
"###);

Ok(())
}
4 changes: 2 additions & 2 deletions docs/guides/integration/pytorch.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ CUDA).
From a packaging perspective, PyTorch has a few uncommon characteristics:

- Many PyTorch wheels are hosted on a dedicated index, rather than the Python Package Index (PyPI).
As such, installing PyTorch typically often configuring a project to use the PyTorch index.
- PyTorch includes distinct builds for each accelerator (e.g., CPU-only, CUDA). Since there's no
As such, installing PyTorch often requires configuring a project to use the PyTorch index.
- PyTorch produces distinct builds for each accelerator (e.g., CPU-only, CUDA). Since there's no
standardized mechanism for specifying these accelerators when publishing or installing, PyTorch
encodes them in the local version specifier. As such, PyTorch versions will often look like
`2.5.1+cpu`, `2.5.1+cu121`, etc.
Expand Down

0 comments on commit 950a81a

Please sign in to comment.