From c58a3541e38fc601d1481041364c014f342eefa6 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Wed, 8 Oct 2025 20:00:41 +0200 Subject: [PATCH 1/3] fix: normalize extra names in optional dependencies --- src/resolution.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/resolution.rs b/src/resolution.rs index 9cc665f..c90996d 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -1,9 +1,17 @@ use crate::{DependencyGroupSpecifier, DependencyGroups, ResolvedDependencies}; use indexmap::IndexMap; -use pep508_rs::Requirement; +use pep508_rs::{ExtraName, Requirement}; use std::fmt::Display; +use std::str::FromStr; use thiserror::Error; +/// Normalize a group/extra name according to PEP 685. +fn normalize_name(name: &str) -> String { + ExtraName::from_str(name) + .map(|extra| extra.to_string()) + .unwrap_or_else(|_| name.to_string()) +} + #[derive(Debug, Error)] #[error(transparent)] pub struct ResolveError(#[from] ResolveErrorKind); @@ -105,7 +113,16 @@ fn resolve_optional_dependency( return Ok(requirements.clone()); } - let Some(unresolved_requirements) = optional_dependencies.get(extra) else { + // Normalize the extra name for lookup according to PEP 685 + let normalized_extra = normalize_name(extra); + + // Find the key in optional_dependencies by comparing normalized versions + let unresolved_requirements = optional_dependencies + .iter() + .find(|(key, _)| normalize_name(key) == normalized_extra) + .map(|(_, reqs)| reqs); + + let Some(unresolved_requirements) = unresolved_requirements else { let parent = parents .iter() .last() @@ -460,4 +477,38 @@ mod tests { vec![Requirement::from_str("numpy").unwrap()] ); } + + #[test] + fn optional_dependencies_with_underscores() { + // Test that optional dependency group names with underscores are normalized + // when referenced in extras. PEP 685 specifies that extras should be normalized + // by replacing _, ., - with a single -. + let source = r#" + [project] + name = "foo" + + [project.optional-dependencies] + all = [ + "foo[group-one]", + "foo[group_two]", + ] + group-one = [ + "anyio>=4.9.0", + ] + group_two = [ + "trio>=0.31.0", + ] + "#; + let pyproject_toml = PyProjectToml::new(source).unwrap(); + let resolved_dependencies = pyproject_toml.resolve().unwrap(); + + // Both group-one and group_two should resolve correctly + assert_eq!( + resolved_dependencies.optional_dependencies["all"], + vec![ + Requirement::from_str("anyio>=4.9.0").unwrap(), + Requirement::from_str("trio>=0.31.0").unwrap(), + ] + ); + } } From 8b90979bf7732a8b99024203395a394c18d42e4d Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Wed, 8 Oct 2025 20:04:36 +0200 Subject: [PATCH 2/3] fix: some comments --- src/resolution.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolution.rs b/src/resolution.rs index c90996d..122e084 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -113,10 +113,10 @@ fn resolve_optional_dependency( return Ok(requirements.clone()); } - // Normalize the extra name for lookup according to PEP 685 let normalized_extra = normalize_name(extra); // Find the key in optional_dependencies by comparing normalized versions + // TODO: next breaking release remove this once Extra is added let unresolved_requirements = optional_dependencies .iter() .find(|(key, _)| normalize_name(key) == normalized_extra) From 8c75b27d2aba26054f6d2d7811de9c07c40abdb4 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Thu, 9 Oct 2025 11:28:45 +0200 Subject: [PATCH 3/3] fix: change test a bit --- src/resolution.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolution.rs b/src/resolution.rs index 122e084..f7507ec 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -492,10 +492,10 @@ mod tests { "foo[group-one]", "foo[group_two]", ] - group-one = [ + group_one = [ "anyio>=4.9.0", ] - group_two = [ + group-two = [ "trio>=0.31.0", ] "#;