|
1 | 1 | use crate::{DependencyGroupSpecifier, DependencyGroups, ResolvedDependencies}; |
2 | 2 | use indexmap::IndexMap; |
3 | | -use pep508_rs::Requirement; |
| 3 | +use pep508_rs::{ExtraName, Requirement}; |
4 | 4 | use std::fmt::Display; |
| 5 | +use std::str::FromStr; |
5 | 6 | use thiserror::Error; |
6 | 7 |
|
| 8 | +/// Normalize a group/extra name according to PEP 685. |
| 9 | +fn normalize_name(name: &str) -> String { |
| 10 | + ExtraName::from_str(name) |
| 11 | + .map(|extra| extra.to_string()) |
| 12 | + .unwrap_or_else(|_| name.to_string()) |
| 13 | +} |
| 14 | + |
7 | 15 | #[derive(Debug, Error)] |
8 | 16 | #[error(transparent)] |
9 | 17 | pub struct ResolveError(#[from] ResolveErrorKind); |
@@ -105,7 +113,16 @@ fn resolve_optional_dependency( |
105 | 113 | return Ok(requirements.clone()); |
106 | 114 | } |
107 | 115 |
|
108 | | - let Some(unresolved_requirements) = optional_dependencies.get(extra) else { |
| 116 | + let normalized_extra = normalize_name(extra); |
| 117 | + |
| 118 | + // Find the key in optional_dependencies by comparing normalized versions |
| 119 | + // TODO: next breaking release remove this once Extra is added |
| 120 | + let unresolved_requirements = optional_dependencies |
| 121 | + .iter() |
| 122 | + .find(|(key, _)| normalize_name(key) == normalized_extra) |
| 123 | + .map(|(_, reqs)| reqs); |
| 124 | + |
| 125 | + let Some(unresolved_requirements) = unresolved_requirements else { |
109 | 126 | let parent = parents |
110 | 127 | .iter() |
111 | 128 | .last() |
@@ -460,4 +477,38 @@ mod tests { |
460 | 477 | vec![Requirement::from_str("numpy").unwrap()] |
461 | 478 | ); |
462 | 479 | } |
| 480 | + |
| 481 | + #[test] |
| 482 | + fn optional_dependencies_with_underscores() { |
| 483 | + // Test that optional dependency group names with underscores are normalized |
| 484 | + // when referenced in extras. PEP 685 specifies that extras should be normalized |
| 485 | + // by replacing _, ., - with a single -. |
| 486 | + let source = r#" |
| 487 | + [project] |
| 488 | + name = "foo" |
| 489 | +
|
| 490 | + [project.optional-dependencies] |
| 491 | + all = [ |
| 492 | + "foo[group-one]", |
| 493 | + "foo[group_two]", |
| 494 | + ] |
| 495 | + group_one = [ |
| 496 | + "anyio>=4.9.0", |
| 497 | + ] |
| 498 | + group-two = [ |
| 499 | + "trio>=0.31.0", |
| 500 | + ] |
| 501 | + "#; |
| 502 | + let pyproject_toml = PyProjectToml::new(source).unwrap(); |
| 503 | + let resolved_dependencies = pyproject_toml.resolve().unwrap(); |
| 504 | + |
| 505 | + // Both group-one and group_two should resolve correctly |
| 506 | + assert_eq!( |
| 507 | + resolved_dependencies.optional_dependencies["all"], |
| 508 | + vec![ |
| 509 | + Requirement::from_str("anyio>=4.9.0").unwrap(), |
| 510 | + Requirement::from_str("trio>=0.31.0").unwrap(), |
| 511 | + ] |
| 512 | + ); |
| 513 | + } |
463 | 514 | } |
0 commit comments