diff --git a/crates/pypi-types/src/lenient_requirement.rs b/crates/pypi-types/src/lenient_requirement.rs index 1b2b9fd1b..2564a3ee9 100644 --- a/crates/pypi-types/src/lenient_requirement.rs +++ b/crates/pypi-types/src/lenient_requirement.rs @@ -15,13 +15,13 @@ static MISSING_COMMA: Lazy = Lazy::new(|| Regex::new(r"(\d)([<>=~^!])").u static NOT_EQUAL_TILDE: Lazy = Lazy::new(|| Regex::new(r"!=~((?:\d\.)*\d)").unwrap()); /// Ex) `>=1.9.*`, `<3.4.*` static INVALID_TRAILING_DOT_STAR: Lazy = - Lazy::new(|| Regex::new(r"(<=|>=|<|>)(\d+(\.\d+)?)\.\*").unwrap()); + Lazy::new(|| Regex::new(r"(<=|>=|<|>)(\d+(\.\d+)*)\.\*").unwrap()); /// Ex) `!=3.0*` static MISSING_DOT: Lazy = Lazy::new(|| Regex::new(r"(\d\.\d)+\*").unwrap()); /// Ex) `>=3.6,` static TRAILING_COMMA: Lazy = Lazy::new(|| Regex::new(r",\s*$").unwrap()); /// Ex) `>= '2.7'`, `>=3.6'` -static STRAY_QUOTES: Lazy = Lazy::new(|| Regex::new(r#"['"]"#).unwrap()); +static STRAY_QUOTES: Lazy = Lazy::new(|| Regex::new(r#"['"]([*\d])|([*\d])['"]"#).unwrap()); /// Regex to match the invalid specifier, replacement to fix it and message about was wrong and /// fixed @@ -45,7 +45,7 @@ static FIXUPS: &[(&Lazy, &str, &str)] = &[ // Given `>=3.6,`, rewrite to `>=3.6` (&TRAILING_COMMA, r"${1}", "removing trailing comma"), // Given `>= '2.7'`, rewrite to `>= 2.7` - (&STRAY_QUOTES, r"", "removing stray quotes"), + (&STRAY_QUOTES, r"$1$2", "removing stray quotes"), ]; fn parse_with_fixups>(input: &str, type_name: &str) -> Result { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 21d0a0eac..9a7d4f78c 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1097,3 +1097,35 @@ fn install_constraints_inline() -> Result<()> { Ok(()) } + +/// Tests that we can install `polars==0.14.0`, which has this odd dependency +/// requirement in its wheel metadata: `pyarrow>=4.0.*; extra == 'pyarrow'`. +/// +/// The `>=4.0.*` is invalid, but is something we "fix" because it is out +/// of the control of the end user. However, our fix for this case ends up +/// stripping the quotes around `pyarrow` and thus produces an irrevocably +/// invalid dependency requirement. +/// +/// See: +#[test] +fn install_pinned_polars_invalid_metadata() { + let context = TestContext::new("3.12"); + + // Install Flask. + uv_snapshot!(command(&context) + .arg("polars==0.14.0"), + @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + polars==0.14.0 + "### + ); + + context.assert_command("import polars").success(); +}