Skip to content

Commit 6b4a6e9

Browse files
Some fixes
1 parent 4432968 commit 6b4a6e9

File tree

2 files changed

+75
-11
lines changed

2 files changed

+75
-11
lines changed

src/has_recursion.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ pub enum RecursionResolutionError {
9393
GroupNotFound(String, String, String),
9494
#[error("Detected a cycle in `{0}`: {1}")]
9595
DependencyGroupCycle(String, Cycle),
96+
#[error(
97+
"Group `{0}` is defined in both `project.optional-dependencies` and `dependency-groups`"
98+
)]
99+
NameCollision(String),
96100
}
97101

98102
/// A cycle in the recursion.

src/resolution.rs

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,49 @@ use pep508_rs::Requirement;
44
use crate::has_recursion::{HasRecursion, Item, RecursionItem, RecursionResolutionError};
55
use crate::{DependencyGroupSpecifier, DependencyGroups, OptionalDependencies, PyProjectToml};
66

7+
type ResolvedDependencies = IndexMap<String, Vec<Requirement>>;
8+
79
impl PyProjectToml {
810
/// Resolve the optional dependencies and dependency groups into flat lists of requirements.
911
///
1012
/// This function will recursively resolve all optional dependency groups and dependency groups,
11-
/// including those that reference other groups. It will return an error if there is a cycle
12-
/// in the groups or if a group references another group that does not exist.
13+
/// including those that reference other groups. It will return an error if
14+
/// - there is a cycle in the groups,
15+
/// - a group references another group that does not exist, or
16+
/// - there is a name collision between optional dependencies and dependency groups.
1317
pub fn resolve(
1418
&self,
1519
) -> Result<
16-
(
17-
Option<IndexMap<String, Vec<Requirement>>>,
18-
Option<IndexMap<String, Vec<Requirement>>>,
19-
),
20+
(Option<ResolvedDependencies>, Option<ResolvedDependencies>),
2021
RecursionResolutionError,
2122
> {
2223
let self_reference_name = self.project.as_ref().map(|p| p.name.as_str());
23-
24-
// Resolve optional dependencies first, as they may be referenced by dependency groups.
25-
let resolved_optional_dependencies = self
24+
let optional_dependencies = self
2625
.project
2726
.as_ref()
28-
.and_then(|project| project.optional_dependencies.as_ref())
27+
.and_then(|p| p.optional_dependencies.as_ref());
28+
29+
// Check for collisions between optional dependencies and dependency groups.
30+
if let (Some(optional_dependencies), Some(dependency_groups)) =
31+
(optional_dependencies, self.dependency_groups.as_ref())
32+
{
33+
for group in optional_dependencies.keys() {
34+
if dependency_groups.contains_key(group) {
35+
return Err(RecursionResolutionError::NameCollision(group.clone()));
36+
}
37+
}
38+
}
39+
40+
// Resolve optional dependencies first, as they may be referenced by dependency groups.
41+
let resolved_optional_dependencies = optional_dependencies
2942
.map(|optional_dependencies| {
3043
optional_dependencies.resolve_all(self_reference_name, None)
3144
})
3245
.transpose()?;
3346

3447
// Resolve dependency groups, which may reference optional dependencies.
35-
let resolved_dependency_groups = self
48+
// At this stage, dependency_groups will contain optional dependency groups as well.
49+
let mut resolved_dependency_groups = self
3650
.dependency_groups
3751
.as_ref()
3852
.map(|dependency_groups| {
@@ -41,6 +55,15 @@ impl PyProjectToml {
4155
})
4256
.transpose()?;
4357

58+
// Remove optional dependency groups from the resolved dependency groups.
59+
if let (Some(resolved_groups), Some(optional_dependencies)) =
60+
(resolved_dependency_groups.as_mut(), optional_dependencies)
61+
{
62+
for key in optional_dependencies.keys() {
63+
resolved_groups.shift_remove(key);
64+
}
65+
}
66+
4467
Ok((resolved_optional_dependencies, resolved_dependency_groups))
4568
}
4669
}
@@ -223,4 +246,41 @@ dev = ["spam[test]"]
223246
vec![Requirement::from_str("pytest").unwrap()]
224247
);
225248
}
249+
250+
#[test]
251+
fn test_name_collision() {
252+
let source = r#"[project]
253+
name = "spam"
254+
255+
[project.optional-dependencies]
256+
dev = ["pytest"]
257+
258+
[dependency-groups]
259+
dev = ["ruff"]
260+
"#;
261+
let project_toml = PyProjectToml::new(source).unwrap();
262+
let err = project_toml.resolve().unwrap_err();
263+
assert_eq!(
264+
err.to_string(),
265+
"Group `dev` is defined in both `project.optional-dependencies` and `dependency-groups`"
266+
);
267+
}
268+
269+
#[test]
270+
fn test_optional_dependencies_are_not_dependency_groups() {
271+
let source = r#"[project]
272+
name = "spam"
273+
274+
[project.optional-dependencies]
275+
test = ["pytest"]
276+
277+
[dependency-groups]
278+
dev = ["spam[test]"]
279+
"#;
280+
let project_toml = PyProjectToml::new(source).unwrap();
281+
let (optional_dependencies, dependency_groups) = project_toml.resolve().unwrap();
282+
assert!(optional_dependencies.unwrap().contains_key("test"));
283+
assert!(!dependency_groups.as_ref().unwrap().contains_key("test"));
284+
assert!(dependency_groups.unwrap().contains_key("dev"));
285+
}
226286
}

0 commit comments

Comments
 (0)