Skip to content

Commit ae5c77c

Browse files
authored
Reject requires python even if not listed on the index page (#13086)
Reject distributions with an incompatible `Requires-Python`, even if the index page is missing `data-requires-python`. Fixes #13079
1 parent cd76210 commit ae5c77c

File tree

2 files changed

+93
-6
lines changed

2 files changed

+93
-6
lines changed

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,6 +1771,16 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
17711771
}
17721772
};
17731773

1774+
// If there was no requires-python on the index page, we may have an incompatible
1775+
// distribution.
1776+
if let Some(requires_python) = &metadata.requires_python {
1777+
if !python_requirement.target().is_contained_by(requires_python) {
1778+
return Ok(Dependencies::Unavailable(
1779+
UnavailableVersion::RequiresPython(requires_python.clone()),
1780+
));
1781+
}
1782+
}
1783+
17741784
let requirements = self.flatten_requirements(
17751785
&metadata.requires_dist,
17761786
&metadata.dependency_groups,

crates/uv/tests/it/pip_compile.rs

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ use flate2::write::GzEncoder;
1111
use fs_err::File;
1212
use indoc::indoc;
1313
use url::Url;
14+
use wiremock::matchers::{method, path};
15+
use wiremock::{Mock, MockServer, ResponseTemplate};
1416

15-
use crate::common::{download_to_disk, packse_index_url, uv_snapshot, TestContext};
1617
use uv_fs::Simplified;
1718
use uv_static::EnvVars;
1819

20+
use crate::common::{download_to_disk, packse_index_url, uv_snapshot, TestContext};
21+
1922
#[test]
2023
fn compile_requirements_in() -> Result<()> {
2124
let context = TestContext::new("3.12");
@@ -9508,7 +9511,7 @@ fn universal_marker_propagation() -> Result<()> {
95089511
.arg("requirements.in")
95099512
.arg("-p")
95109513
.arg("3.8")
9511-
.arg("--universal"), @r###"
9514+
.arg("--universal"), @r"
95129515
success: true
95139516
exit_code: 0
95149517
----- stdout -----
@@ -9536,9 +9539,11 @@ fn universal_marker_propagation() -> Result<()> {
95369539
# via jinja2
95379540
mpmath==1.3.0
95389541
# via sympy
9539-
networkx==3.2.1
9542+
networkx==3.1 ; python_full_version < '3.9'
9543+
# via torch
9544+
networkx==3.2 ; python_full_version >= '3.9'
95409545
# via torch
9541-
numpy==1.26.3 ; python_full_version < '3.9'
9546+
numpy==1.24.4 ; python_full_version < '3.9'
95429547
# via torchvision
95439548
numpy==1.26.4 ; python_full_version >= '3.9'
95449549
# via torchvision
@@ -9576,8 +9581,8 @@ fn universal_marker_propagation() -> Result<()> {
95769581

95779582
----- stderr -----
95789583
warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead.
9579-
Resolved 25 packages in [TIME]
9580-
"###
9584+
Resolved 26 packages in [TIME]
9585+
"
95819586
);
95829587

95839588
Ok(())
@@ -17223,3 +17228,75 @@ fn pep_751_compile_no_emit_package() -> Result<()> {
1722317228

1722417229
Ok(())
1722517230
}
17231+
17232+
/// Check that we reject versions that have an incompatible `Requires-Python`, but don't
17233+
/// have a `data-requires-python` key on the index page.
17234+
#[tokio::test]
17235+
async fn index_has_no_requires_python() -> Result<()> {
17236+
let context = TestContext::new_with_versions(&["3.9", "3.12"]);
17237+
let server = MockServer::start().await;
17238+
17239+
// Unlike PyPI, https://download.pytorch.org/whl/cpu/networkx/ does not contain the
17240+
// `data-requires-python` key.
17241+
let networkx_page = r#"
17242+
<!DOCTYPE html>
17243+
<html>
17244+
<body>
17245+
<h1>Links for networkx</h1>
17246+
<a href="https://download.pytorch.org/whl/networkx-3.0-py3-none-any.whl#sha256=58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e">networkx-3.0-py3-none-any.whl</a><br/>
17247+
<a href="https://download.pytorch.org/whl/networkx-3.2.1-py3-none-any.whl#sha256=f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2">networkx-3.2.1-py3-none-any.whl</a><br/>
17248+
<a href="https://download.pytorch.org/whl/networkx-3.3-py3-none-any.whl#sha256=28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2">networkx-3.3-py3-none-any.whl</a><br/>
17249+
</body>
17250+
</html>
17251+
"#;
17252+
Mock::given(method("GET"))
17253+
.and(path("/networkx/"))
17254+
.respond_with(ResponseTemplate::new(200).set_body_raw(networkx_page, "text/html"))
17255+
.mount(&server)
17256+
.await;
17257+
17258+
let requirements_in = context.temp_dir.child("requirements.in");
17259+
requirements_in.write_str("networkx >3.0,<=3.3")?;
17260+
17261+
uv_snapshot!(context
17262+
.pip_compile()
17263+
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
17264+
.arg("--python")
17265+
.arg("3.9")
17266+
.arg("--index-url")
17267+
.arg(server.uri())
17268+
.arg("requirements.in"), @r"
17269+
success: true
17270+
exit_code: 0
17271+
----- stdout -----
17272+
# This file was autogenerated by uv via the following command:
17273+
# uv pip compile --cache-dir [CACHE_DIR] --python 3.9 requirements.in
17274+
networkx==3.2.1
17275+
# via -r requirements.in
17276+
17277+
----- stderr -----
17278+
Resolved 1 package in [TIME]
17279+
");
17280+
17281+
uv_snapshot!(context
17282+
.pip_compile()
17283+
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
17284+
.arg("--python")
17285+
.arg("3.12")
17286+
.arg("--index-url")
17287+
.arg(server.uri())
17288+
.arg("requirements.in"), @r"
17289+
success: true
17290+
exit_code: 0
17291+
----- stdout -----
17292+
# This file was autogenerated by uv via the following command:
17293+
# uv pip compile --cache-dir [CACHE_DIR] --python 3.12 requirements.in
17294+
networkx==3.3
17295+
# via -r requirements.in
17296+
17297+
----- stderr -----
17298+
Resolved 1 package in [TIME]
17299+
");
17300+
17301+
Ok(())
17302+
}

0 commit comments

Comments
 (0)