From 4bb219f8b91fb1d5ecc78617a577f463f1fa1529 Mon Sep 17 00:00:00 2001 From: Nicola Soranzo Date: Wed, 26 Nov 2025 03:13:32 +0000 Subject: [PATCH] Fix ``uv pip install -r /dev/stdin`` (#16855) ## Summary Fix ``uv pip install -r /dev/stdin`` which was broken in uv 0.9.12 by https://github.com/astral-sh/uv/pull/16805 . Example of the issue: ``` $ echo "flask" | uv pip install -r /dev/stdin warning: Requirements file `/dev/stdin` does not contain any dependencies Audited in 8ms ``` Note that "upstream" ``pip install`` does support `-r /dev/stdin` and doesn't support `-r -` . ## Test Plan 2 new tests added. --- crates/uv-requirements/src/sources.rs | 2 +- crates/uv/tests/it/pip_install.rs | 77 ++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 1ef25d399..86fd14661 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -65,7 +65,7 @@ impl RequirementsSource { .is_some_and(|ext| ext.eq_ignore_ascii_case("txt") || ext.eq_ignore_ascii_case("in")) { Ok(Self::RequirementsTxt(path)) - } else if path == Path::new("-") { + } else if path == Path::new("-") || path == Path::new("/dev/stdin") { // If the path is `-`, treat it as a requirements.txt file from stdin. Ok(Self::RequirementsTxt(path)) } else if path.extension().is_none() { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index e6d9b2672..8441f7c38 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -555,7 +555,7 @@ fn install_requirements_txt() -> Result<()> { context.assert_command("import flask").success(); - // Install Jinja2 (which should already be installed, but shouldn't remove other packages). + // Install iniconfig (which shouldn't remove other packages). let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("iniconfig")?; @@ -580,6 +580,81 @@ fn install_requirements_txt() -> Result<()> { Ok(()) } +/// Install a package from a `requirements.txt` passed via `-r -` into a virtual environment. +#[test] +#[allow(clippy::disallowed_types)] +fn install_from_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + + // Install Flask. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("Flask")?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("-") + .arg("--strict").stdin(std::fs::File::open(requirements_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 + "### + ); + + context.assert_command("import flask").success(); + + Ok(()) +} + +/// Install a package from a `requirements.txt` passed via `-r /dev/stdin` into a virtual environment. +#[test] +#[cfg(not(windows))] +#[allow(clippy::disallowed_types)] +fn install_from_dev_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + + // Install Flask. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("Flask")?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("/dev/stdin") + .arg("--strict").stdin(std::fs::File::open(requirements_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 7 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.2 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 + "### + ); + + context.assert_command("import flask").success(); + + Ok(()) +} + /// Install a package from a remote `requirements.txt` into a virtual environment. #[tokio::test] async fn install_remote_requirements_txt() -> Result<()> {