-
Notifications
You must be signed in to change notification settings - Fork 3
Only require setuptools when an initial attempt fails #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
142a1fa
Only require setuptools when an initial probe fails
bradhe f9b9132
All dependencies come from workspace
bradhe cbf3315
Make commentary a little less specific
bradhe 961a7de
Fall back to doing a pinned version install only if the first one fails
bradhe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -357,44 +357,85 @@ impl Uv { | |
| &self.uv_path, cwd | ||
| ); | ||
|
|
||
| // If there is a requirements.txt, then we can use that to sync. | ||
| let mut cmd = Command::new(&self.uv_path); | ||
| cmd.kill_on_drop(true) | ||
| .stdin(Stdio::null()) | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()) | ||
| .current_dir(cwd) | ||
| .arg("--color") | ||
| .arg("never") | ||
| .arg("pip") | ||
| .arg("install") | ||
| .arg("-r") | ||
| .arg(cwd.join("requirements.txt")) | ||
| // setuptools 82 removed pkg_resources, but many legacy packages | ||
| // still import it without declaring the dependency. Let's always install | ||
| // a version that includes pkg_resources for requirements.txt, on the | ||
| // basis that requirements.txt projects are probably not using the latest | ||
| // and greatest deps (then they'd likely be using pyproject.toml anyway) | ||
| // https://github.com/pypa/setuptools/issues/5174 | ||
| .arg("setuptools<82") | ||
| .envs(env_vars); | ||
| self.spawn_requirements_install(cwd, env_vars, false).await | ||
| } else { | ||
| // If there is no pyproject.toml or requirements.txt, then we can't sync. | ||
| Err(Error::MissingPyprojectToml) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| { | ||
| cmd.process_group(0); | ||
| } | ||
| /// Returns whether a failed `sync()` for this directory is eligible for a | ||
| /// retry via [`sync_with_legacy_setuptools_pin`]. Only applies to projects | ||
| /// driven by `requirements.txt`; pyproject-based projects manage their own | ||
| /// setuptools dependency. | ||
| pub fn should_use_legacy_setuptools_pin(&self, cwd: &Path) -> bool { | ||
| cwd.join("requirements.txt").exists() | ||
| } | ||
|
Comment on lines
+367
to
+373
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we're doing the retesting method we have no reason to differentiate |
||
|
|
||
| if let Some(dir) = &self.cache_dir { | ||
| cmd.arg("--cache-dir").arg(dir); | ||
| } | ||
| /// Re-runs the `requirements.txt` install with a `setuptools<82` pin appended. | ||
| /// | ||
| /// setuptools 82 removed `pkg_resources`, but many legacy packages still import | ||
| /// it without declaring the dependency. Pinning `setuptools<82` keeps it | ||
| /// available. Some modern packages (e.g. dlt's transitive graph pinning | ||
| /// `setuptools==82.0.1`) make this pin unsatisfiable, so it isn't applied up | ||
| /// front — callers should fall back to this only after a plain `sync()` | ||
| /// fails for a project using `requirements.txt`. | ||
| /// | ||
| /// https://github.com/pypa/setuptools/issues/5174 | ||
| pub async fn sync_with_legacy_setuptools_pin( | ||
| &self, | ||
| cwd: &PathBuf, | ||
| env_vars: &HashMap<String, String>, | ||
| ) -> Result<Child, Error> { | ||
| if !cwd.join("requirements.txt").exists() { | ||
| return Err(Error::MissingPyprojectToml); | ||
| } | ||
|
|
||
| let child = cmd.spawn()?; | ||
| debug!( | ||
| "Retrying UV ({:?}) sync with setuptools<82 pin in {:?}", | ||
| &self.uv_path, cwd | ||
| ); | ||
|
|
||
| Ok(child) | ||
| } else { | ||
| // If there is no pyproject.toml or requirements.txt, then we can't sync. | ||
| Err(Error::MissingPyprojectToml) | ||
| self.spawn_requirements_install(cwd, env_vars, true).await | ||
| } | ||
|
|
||
| async fn spawn_requirements_install( | ||
| &self, | ||
| cwd: &PathBuf, | ||
| env_vars: &HashMap<String, String>, | ||
| pin_legacy_setuptools: bool, | ||
| ) -> Result<Child, Error> { | ||
| let req_path = cwd.join("requirements.txt"); | ||
|
|
||
| let mut cmd = Command::new(&self.uv_path); | ||
| cmd.kill_on_drop(true) | ||
| .stdin(Stdio::null()) | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()) | ||
| .current_dir(cwd) | ||
| .arg("--color") | ||
| .arg("never") | ||
| .arg("pip") | ||
| .arg("install") | ||
| .arg("-r") | ||
| .arg(&req_path); | ||
|
|
||
| if pin_legacy_setuptools { | ||
| cmd.arg("setuptools<82"); | ||
| } | ||
|
|
||
| cmd.envs(env_vars); | ||
|
|
||
| #[cfg(unix)] | ||
| { | ||
| cmd.process_group(0); | ||
| } | ||
|
|
||
| if let Some(dir) = &self.cache_dir { | ||
| cmd.arg("--cache-dir").arg(dir); | ||
| } | ||
|
|
||
| Ok(cmd.spawn()?) | ||
| } | ||
|
|
||
| pub async fn run( | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| //! Integration tests for `Uv::sync` requirements.txt handling. | ||
| //! | ||
| //! `sync()` runs a plain `uv pip install -r requirements.txt` (no setuptools | ||
| //! pin). Callers that hit a resolution failure can retry via | ||
| //! `sync_with_legacy_setuptools_pin()` for the legacy `pkg_resources` | ||
| //! compatibility case. The retry orchestration lives in | ||
| //! `tower-runtime::local`. | ||
| //! | ||
| //! These tests shell out to a real `uv` binary and hit pypi, mirroring the | ||
| //! existing `install_test.rs`. | ||
|
|
||
| use std::collections::HashMap; | ||
| use std::path::PathBuf; | ||
|
|
||
| use tempfile::TempDir; | ||
| use tokio::process::Child; | ||
| use tower_uv::Uv; | ||
|
|
||
| async fn wait(mut child: Child) -> i32 { | ||
| let status = child.wait().await.expect("wait failed"); | ||
| status.code().unwrap_or(-1) | ||
| } | ||
|
|
||
| async fn make_uv_with_venv(cwd: &PathBuf) -> Uv { | ||
| let uv = Uv::new(None, false).await.expect("Uv::new failed"); | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let venv_child = uv.venv(cwd, &env_vars).await.expect("venv spawn failed"); | ||
| let code = wait(venv_child).await; | ||
| assert_eq!(code, 0, "venv creation failed"); | ||
| uv | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn sync_succeeds_for_simple_requirements() { | ||
| let tmp = TempDir::new().expect("tempdir"); | ||
| let cwd = tmp.path().to_path_buf(); | ||
|
|
||
| tokio::fs::write(cwd.join("requirements.txt"), "six\n") | ||
| .await | ||
| .expect("write requirements.txt"); | ||
|
|
||
| let uv = make_uv_with_venv(&cwd).await; | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let child = uv.sync(&cwd, &env_vars).await.expect("sync spawn failed"); | ||
| let code = wait(child).await; | ||
| assert_eq!(code, 0, "sync should succeed for a simple requirements.txt"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn sync_succeeds_when_user_requires_modern_setuptools() { | ||
| let tmp = TempDir::new().expect("tempdir"); | ||
| let cwd = tmp.path().to_path_buf(); | ||
|
|
||
| // Regression case: an app that requires setuptools>=82 used to fail | ||
| // because tower-uv unconditionally injected `setuptools<82`. Now the | ||
| // default sync path applies no pin, so resolution should succeed. | ||
| tokio::fs::write(cwd.join("requirements.txt"), "setuptools>=82\n") | ||
| .await | ||
| .expect("write requirements.txt"); | ||
|
|
||
| let uv = make_uv_with_venv(&cwd).await; | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let child = uv.sync(&cwd, &env_vars).await.expect("sync spawn failed"); | ||
| let code = wait(child).await; | ||
| assert_eq!( | ||
| code, 0, | ||
| "sync should succeed when the user requires setuptools>=82" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn sync_with_legacy_setuptools_pin_installs_legacy_setuptools() { | ||
| let tmp = TempDir::new().expect("tempdir"); | ||
| let cwd = tmp.path().to_path_buf(); | ||
|
|
||
| tokio::fs::write(cwd.join("requirements.txt"), "six\n") | ||
| .await | ||
| .expect("write requirements.txt"); | ||
|
|
||
| let uv = make_uv_with_venv(&cwd).await; | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let child = uv | ||
| .sync_with_legacy_setuptools_pin(&cwd, &env_vars) | ||
| .await | ||
| .expect("retry spawn failed"); | ||
| let code = wait(child).await; | ||
| assert_eq!( | ||
| code, 0, | ||
| "sync_with_legacy_setuptools_pin should succeed when the pin is compatible" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn sync_with_legacy_setuptools_pin_fails_when_user_requires_modern_setuptools() { | ||
| let tmp = TempDir::new().expect("tempdir"); | ||
| let cwd = tmp.path().to_path_buf(); | ||
|
|
||
| // The fallback method intentionally pins `setuptools<82`. When the user's | ||
| // requirements demand setuptools>=82, the resolver must report a conflict | ||
| // — this confirms the pin is actually being applied. | ||
| tokio::fs::write(cwd.join("requirements.txt"), "setuptools>=82\n") | ||
| .await | ||
| .expect("write requirements.txt"); | ||
|
|
||
| let uv = make_uv_with_venv(&cwd).await; | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let child = uv | ||
| .sync_with_legacy_setuptools_pin(&cwd, &env_vars) | ||
| .await | ||
| .expect("retry spawn failed"); | ||
| let code = wait(child).await; | ||
| assert_ne!( | ||
| code, 0, | ||
| "sync_with_legacy_setuptools_pin should fail when the pin conflicts with user's requirements" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn sync_with_legacy_setuptools_pin_errors_without_requirements_txt() { | ||
| let tmp = TempDir::new().expect("tempdir"); | ||
| let cwd = tmp.path().to_path_buf(); | ||
|
|
||
| let uv = Uv::new(None, false).await.expect("Uv::new failed"); | ||
| let env_vars: HashMap<String, String> = HashMap::new(); | ||
| let result = uv.sync_with_legacy_setuptools_pin(&cwd, &env_vars).await; | ||
| assert!( | ||
| matches!(result, Err(tower_uv::Error::MissingPyprojectToml)), | ||
| "fallback should refuse to run without a requirements.txt" | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could probably take this whole bit out into its own helper function now, but not a blocker