From 43394e39087877af89a2ca83b29f2e4fec05a83d Mon Sep 17 00:00:00 2001 From: Jop Zitman Date: Fri, 19 Dec 2025 11:30:39 +0100 Subject: [PATCH 1/3] Handle "-" path as extensionless Since #16923, `-` stdin paths are only supported on the `RequirementsSource::Extensionless`. However, parsing of cli arguments using `from_requirements_txt`, `from_constraints_txt` `from_overrides_txt` would always output a `RequirementsSource::RequirementsTxt`. I've added a small check in those for the `-` paths to use `ExtensionLess`. I'm not too sure about this change, as it would also implicitly start allowing PEP 723 scripts as input to overrides/constraints. I don't see the direct issue in that, but then maybe we should explicitly handle it so that an `--overrides=script.py` would also be supported. --- crates/uv-requirements/src/sources.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 5bca4a22a170c..d3b898f9a722a 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -78,6 +78,10 @@ impl RequirementsSource { /// Parse a [`RequirementsSource`] from a `requirements.txt` file. pub fn from_requirements_txt(path: PathBuf) -> Result { + if path == Path::new("-") { + return Ok(Self::Extensionless(path)); + } + for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { return Err(anyhow::anyhow!( @@ -110,6 +114,10 @@ impl RequirementsSource { /// Parse a [`RequirementsSource`] from a `constraints.txt` file. pub fn from_constraints_txt(path: PathBuf) -> Result { + if path == Path::new("-") { + return Ok(Self::Extensionless(path)); + } + for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { return Err(anyhow::anyhow!( @@ -142,6 +150,10 @@ impl RequirementsSource { /// Parse a [`RequirementsSource`] from an `overrides.txt` file. pub fn from_overrides_txt(path: PathBuf) -> Result { + if path == Path::new("-") { + return Ok(Self::Extensionless(path)); + } + for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { return Err(anyhow::anyhow!( From 28cd5a4e44b546dc8a94c4672c8356a1bc6b6519 Mon Sep 17 00:00:00 2001 From: Jop Zitman Date: Fri, 19 Dec 2025 16:44:21 +0100 Subject: [PATCH 2/3] Add tests --- crates/uv/tests/it/pip_install.rs | 117 ++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index e7129c40b56ed..6baef9c29febc 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -3561,6 +3561,39 @@ fn install_constraints_txt() -> Result<()> { Ok(()) } +/// Install a package from a `requirements.txt` file, with a `constraints.txt` file. +#[test] +fn install_constraints_txt_from_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==3.7.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("idna<3.4")?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--constraint") + .arg("-") + .stdin(std::fs::File::open(constraints_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.3 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + /// Check that `tool.uv.constraint-dependencies` in `pyproject.toml` is respected. #[test] fn install_constraints_from_pyproject() -> Result<()> { @@ -7303,6 +7336,62 @@ fn require_hashes_override() -> Result<()> { Ok(()) } +/// Install with overrides from stdin. +#[test] +fn install_with_overrides_from_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("anyio==4.0.0")?; + + uv_snapshot!(context.pip_install() + .arg("anyio==4.0.1") + .arg("--override") + .arg("-") + .stdin(std::fs::File::open(overrides_txt)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + +/// Install with excludes from stdin. +#[test] +fn install_with_excludes_from_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + + let excludes_txt = context.temp_dir.child("excludes.txt"); + excludes_txt.write_str("anyio>4.0.0")?; + + uv_snapshot!(context.pip_install() + .arg("anyio==4.0.1") + .arg("--exclude") + .arg("-") + .stdin(std::fs::File::open(excludes_txt)?), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because there is no version of anyio==4.0.1 and you require anyio==4.0.1, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + /// Provide valid hashes for all dependencies with `--require-hashes` with accompanying markers. /// Critically, one package (`requests`) depends on another (`urllib3`). #[test] @@ -8602,6 +8691,34 @@ fn incompatible_build_constraint() -> Result<()> { Ok(()) } +/// Include a `build_constraints.txt` file with an incompatible constraint from stdin. +#[test] +fn incompatible_build_constraint_from_stdin() -> Result<()> { + let context = TestContext::new(DEFAULT_PYTHON_VERSION); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2") + .arg("--build-constraint") + .arg("-") + .stdin(std::fs::File::open(constraints_txt)?), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + /// Include a `build_constraints.txt` file with a compatible constraint. #[test] fn compatible_build_constraint() -> Result<()> { From 676e7f02b40f12a7544527bcdedf7fe5648019dc Mon Sep 17 00:00:00 2001 From: Jop Zitman Date: Fri, 19 Dec 2025 17:13:07 +0100 Subject: [PATCH 3/3] clippy clip --- crates/uv/tests/it/pip_install.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 6baef9c29febc..93309dd6b0fae 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -3563,6 +3563,7 @@ fn install_constraints_txt() -> Result<()> { /// Install a package from a `requirements.txt` file, with a `constraints.txt` file. #[test] +#[allow(clippy::disallowed_types)] fn install_constraints_txt_from_stdin() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); @@ -7338,6 +7339,7 @@ fn require_hashes_override() -> Result<()> { /// Install with overrides from stdin. #[test] +#[allow(clippy::disallowed_types)] fn install_with_overrides_from_stdin() -> Result<()> { let context = TestContext::new("3.12"); @@ -7368,6 +7370,7 @@ fn install_with_overrides_from_stdin() -> Result<()> { /// Install with excludes from stdin. #[test] +#[allow(clippy::disallowed_types)] fn install_with_excludes_from_stdin() -> Result<()> { let context = TestContext::new("3.12"); @@ -8693,6 +8696,7 @@ fn incompatible_build_constraint() -> Result<()> { /// Include a `build_constraints.txt` file with an incompatible constraint from stdin. #[test] +#[allow(clippy::disallowed_types)] fn incompatible_build_constraint_from_stdin() -> Result<()> { let context = TestContext::new(DEFAULT_PYTHON_VERSION);