-
Notifications
You must be signed in to change notification settings - Fork 261
feat: Support PEP 735 dependency groups #823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Reviewer's GuideThis PR implements full support for PEP 735 top-level dependency-groups by reading and normalizing the new “dependency-groups” table, resolving include-group references (with cycle and missing-group checks), merging with existing tool.poetry.group settings, and propagating group metadata into created Dependency instances; it also adds validation via a JSON schema and comprehensive positive and negative tests. ER diagram for dependency-groups and group mergingerDiagram
PROJECT ||--o{ DEPENDENCY_GROUPS : has
DEPENDENCY_GROUPS ||--o{ DEPENDENCY : contains
PROJECT ||--o{ TOOL_POETRY_GROUP : has
TOOL_POETRY_GROUP ||--o{ DEPENDENCY : contains
DEPENDENCY_GROUPS ||--o{ INCLUDE_GROUP : includes
INCLUDE_GROUP {
string include-group
}
DEPENDENCY {
string name
string version
string[] groups
}
Class diagram for updated Dependency creation and group propagationclassDiagram
class Dependency {
+with_groups(groups: Iterable[str]) Dependency
+create_from_pep_508(name: str, relative_to: Path|None = None, groups: Iterable[str]|None = None) Dependency
marker
_constraint
}
class FileDependency {
<<Dependency>>
+groups: Iterable[str]|None
}
class DirectoryDependency {
<<Dependency>>
+groups: Iterable[str]|None
}
class VCSDependency {
<<Dependency>>
+groups: Iterable[str]|None
}
class URLDependency {
<<Dependency>>
+groups: Iterable[str]|None
}
Dependency <|-- FileDependency
Dependency <|-- DirectoryDependency
Dependency <|-- VCSDependency
Dependency <|-- URLDependency
class ProjectPackage {
+add_dependency_group(group: DependencyGroup)
+has_dependency_group(group_name: str) bool
+dependency_group(group_name: str) DependencyGroup
extras
}
class DependencyGroup {
+add_dependency(dep: Dependency)
name
optional
}
ProjectPackage "1" o-- "*" DependencyGroup
DependencyGroup "1" o-- "*" Dependency
Class diagram for Factory dependency group normalization and resolutionclassDiagram
class Factory {
+_normalize_dependency_group_names(dependency_groups: dict) dict
+_resolve_dependency_group_includes(dependency_groups: dict) dict
+_configure_package_dependencies(package, project, tool_poetry, dependency_groups, with_groups)
}
Factory ..> ProjectPackage
Factory ..> DependencyGroup
Factory ..> Dependency
Flow diagram for dependency group normalization and include resolutionflowchart TD
A[Read dependency-groups from pyproject] --> B[Normalize group names]
B --> C[Check for duplicate group names]
C --> D[Resolve include-group references]
D --> E{Cycle or missing group?}
E -- Yes --> F[Raise ValueError]
E -- No --> G[Merge with tool.poetry.group]
G --> H[Create DependencyGroup and add to package]
H --> I[Propagate group info to Dependency instances]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
83710eb
to
6473ff4
Compare
6473ff4
to
e442bb4
Compare
2d7ac12
to
0a50ac3
Compare
@sourcery-ai review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @finswimmer - I've reviewed your changes - here's some feedback:
Overall Comments:
- Please add documentation for the new dependency groups feature - this is a significant change that needs to be well documented for users
- Link to the relevant issue number/tracking issue for PEP 735 implementation in the PR description
Here's what I looked at during the review
- 🟢 General issues: all looks good
- 🟢 Security: all looks good
- 🟢 Testing: all looks good
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
c19b017
to
ed77a26
Compare
@sourcery-ai review |
@sourcery-ai review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @finswimmer - I've reviewed your changes - here's some feedback:
Overall Comments:
- Consider consolidating the two loops in _configure_package_dependencies that handle dependency groups (one for dependency-groups and one for tool.poetry.group) to reduce repeated logic.
- If a dependency group exists in the pyproject data but lacks a corresponding tool.poetry.group entry, consider whether you want to apply default optional settings explicitly.
Here's what I looked at during the review
- 🟢 General issues: all looks good
- 🟢 Security: all looks good
- 🟢 Testing: all looks good
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
ed77a26
to
c34e8b2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @finswimmer - I've reviewed your changes - here's some feedback:
Overall Comments:
- Consider consolidating the two loops in _configure_package_dependencies to reduce code duplication in processing dependency groups.
- Validate and normalize the dependency groups input early to ensure consistent types before iterating over them.
Here's what I looked at during the review
- 🟢 General issues: all looks good
- 🟢 Security: all looks good
- 🟡 Testing: 1 issue found
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
tests/test_factory.py
Outdated
@@ -844,6 +869,19 @@ def test_create_poetry_with_invalid_dev_dependencies(caplog: LogCaptureFixture) | |||
assert any("dev" in r.groups for r in poetry.package.all_requires) | |||
|
|||
|
|||
def test_create_poetry_with_invalid_dependency_groups() -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Test more invalid group configurations.
The current test only covers one invalid case. Consider adding tests for other invalid scenarios, such as incorrect data types for dependency names, invalid characters in group names, or missing required fields.
Suggested implementation:
def test_create_poetry_with_invalid_dependency_groups() -> None:
with pytest.raises(RuntimeError) as e:
_ = Factory().create_poetry(
fixtures_dir / "project_with_invalid_dependency_groups", with_groups=False
)
expected = """\
The Poetry configuration is invalid:
- dependency-groups.testing[1] must be string
"""
assert str(e.value) == expected
def test_create_poetry_with_invalid_dependency_names() -> None:
with pytest.raises(RuntimeError) as e:
_ = Factory().create_poetry(
fixtures_dir / "project_with_invalid_dependency_names"
)
expected = """\
The Poetry configuration is invalid:
- dependencies.some_dependency must be string, got int
"""
assert str(e.value) == expected
def test_create_poetry_with_invalid_group_names() -> None:
with pytest.raises(RuntimeError) as e:
_ = Factory().create_poetry(
fixtures_dir / "project_with_invalid_group_names"
)
expected = """\
The Poetry configuration is invalid:
- dependency-groups.invalid-group-name! contains invalid characters
"""
assert str(e.value) == expected
def test_create_poetry_with_missing_required_fields() -> None:
with pytest.raises(RuntimeError) as e:
_ = Factory().create_poetry(
fixtures_dir / "project_with_missing_required_fields"
)
expected = """\
The Poetry configuration is invalid:
- dependency 'essential' is missing required field 'version'
"""
assert str(e.value) == expected
Make sure to create the corresponding fixture projects under the fixtures directory for each invalid configuration:
- project_with_invalid_dependency_names
- project_with_invalid_group_names
- project_with_missing_required_fields
These changes allow tests to cover multiple invalid scenarios as suggested.
c34e8b2
to
c501df5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support for https://peps.python.org/pep-0735/#dependency-object-specifiers is missing, which might be confusing for users.
I'm sure there are many who are eagerly awaiting this, thank you. |
Has development migrated to #837? Or is this PR blocked on that one? I'm happy to do some work to help this come in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @finswimmer - I've reviewed your changes - here's some feedback:
- The newly added dependency-groups-schema.json file appears empty; please populate it with the actual JSON schema so that validation of
dependency-groups
works correctly. - _configure_package_dependencies is becoming quite large and does multiple passes over groups—consider extracting group initialization, constraint parsing, and include handling into smaller helper methods to improve readability and maintainability.
- _resolve_dependency_group_includes currently mutates the input dict in a loop for cycle detection; switching to a dedicated graph‐based algorithm or working on a copy could make the cycle detection logic clearer and side‐effect free.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The newly added dependency-groups-schema.json file appears empty; please populate it with the actual JSON schema so that validation of `dependency-groups` works correctly.
- _configure_package_dependencies is becoming quite large and does multiple passes over groups—consider extracting group initialization, constraint parsing, and include handling into smaller helper methods to improve readability and maintainability.
- _resolve_dependency_group_includes currently mutates the input dict in a loop for cycle detection; switching to a dedicated graph‐based algorithm or working on a copy could make the cycle detection logic clearer and side‐effect free.
## Individual Comments
### Comment 1
<location> `src/poetry/core/factory.py:453` </location>
<code_context>
+ ) -> dict[NormalizedName, list[NormalizedName]]:
+ resolved_groups: set[NormalizedName] = set()
+ included: dict[NormalizedName, list[NormalizedName]] = defaultdict(list)
+ while resolved_groups != set(dependency_groups):
+ for group, dependencies in dependency_groups.items():
+ if group in resolved_groups:
</code_context>
<issue_to_address>
Possible infinite loop in _resolve_dependency_group_includes if dependency_groups is malformed.
Consider adding a maximum iteration limit or an explicit error if the loop cannot make progress, to prevent potential infinite loops with malformed input.
</issue_to_address>
### Comment 2
<location> `src/poetry/core/factory.py:466` </location>
<code_context>
+ resolved_dependencies.append(dep)
+ else:
+ included_group = canonicalize_name(dep["include-group"])
+ if included_group in included[group]:
+ raise ValueError(
+ f"Cyclic dependency group include:"
</code_context>
<issue_to_address>
Cyclic dependency group detection may not catch all cycles.
The current logic only detects direct cycles. To handle indirect cycles, implement a more comprehensive cycle detection algorithm, such as depth-first search with a visited set.
</issue_to_address>
### Comment 3
<location> `src/poetry/core/factory.py:700` </location>
<code_context>
]
result["errors"] += tool_poetry_validation_errors
+ dependency_groups = toml_data.get("dependency-groups")
+ if dependency_groups is not None:
+ dependency_groups_validation_errors = [
</code_context>
<issue_to_address>
Validation error messages for dependency-groups may be unclear.
Replacing 'data' with 'dependency-groups' in error messages may not always yield clear results, especially for complex schema errors. Please review the formatting to ensure messages remain accurate and user-friendly.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
dependency_groups_validation_errors = [
e.replace("data", "dependency-groups")
for e in validate_object(dependency_groups, "dependency-groups-schema")
]
=======
dependency_groups_validation_errors = [
(
f"dependency-groups: {e[5:]}" if e.startswith("data:") else
e.replace("data", "dependency-groups", 1) if e.startswith("data") else
e
)
for e in validate_object(dependency_groups, "dependency-groups-schema")
]
>>>>>>> REPLACE
</suggested_fix>
### Comment 4
<location> `src/poetry/core/packages/dependency.py:338` </location>
<code_context>
+ @classmethod
+ def _normalize_dependency_group_names(
+ cls,
+ dependency_groups: dict[str, list[str | dict[str, str]]],
+ ) -> dict[NormalizedName, list[str | dict[str, str]]]:
</code_context>
<issue_to_address>
The groups parameter in create_from_pep_508 is not documented.
Please update the docstring to include the groups parameter for clarity.
</issue_to_address>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
) -> dict[NormalizedName, list[NormalizedName]]: | ||
resolved_groups: set[NormalizedName] = set() | ||
included: dict[NormalizedName, list[NormalizedName]] = defaultdict(list) | ||
while resolved_groups != set(dependency_groups): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Possible infinite loop in _resolve_dependency_group_includes if dependency_groups is malformed.
Consider adding a maximum iteration limit or an explicit error if the loop cannot make progress, to prevent potential infinite loops with malformed input.
resolved_dependencies.append(dep) | ||
else: | ||
included_group = canonicalize_name(dep["include-group"]) | ||
if included_group in included[group]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Cyclic dependency group detection may not catch all cycles.
The current logic only detects direct cycles. To handle indirect cycles, implement a more comprehensive cycle detection algorithm, such as depth-first search with a visited set.
dependency_groups_validation_errors = [ | ||
e.replace("data", "dependency-groups") | ||
for e in validate_object(dependency_groups, "dependency-groups-schema") | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Validation error messages for dependency-groups may be unclear.
Replacing 'data' with 'dependency-groups' in error messages may not always yield clear results, especially for complex schema errors. Please review the formatting to ensure messages remain accurate and user-friendly.
dependency_groups_validation_errors = [ | |
e.replace("data", "dependency-groups") | |
for e in validate_object(dependency_groups, "dependency-groups-schema") | |
] | |
dependency_groups_validation_errors = [ | |
( | |
f"dependency-groups: {e[5:]}" if e.startswith("data:") else | |
e.replace("data", "dependency-groups", 1) if e.startswith("data") else | |
e | |
) | |
for e in validate_object(dependency_groups, "dependency-groups-schema") | |
] |
cls, | ||
name: str, | ||
relative_to: Path | None = None, | ||
groups: Iterable[str] | None = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: The groups parameter in create_from_pep_508 is not documented.
Please update the docstring to include the groups parameter for clarity.
errors = [] | ||
for normed_name, names in original_names.items(): | ||
if len(names) > 1: | ||
errors.append(f"{normed_name} ({', '.join(names)})") | ||
if errors: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): We've found these issues:
- Convert for loop into list comprehension (
list-comprehension
) - Use named expression to simplify assignment and conditional (
use-named-expression
)
errors = [] | |
for normed_name, names in original_names.items(): | |
if len(names) > 1: | |
errors.append(f"{normed_name} ({', '.join(names)})") | |
if errors: | |
if errors := [ | |
f"{normed_name} ({', '.join(names)})" | |
for normed_name, names in original_names.items() | |
if len(names) > 1 | |
]: |
try: | ||
resolved_dependencies.extend( | ||
dependency_groups[included_group] | ||
) | ||
except KeyError: | ||
raise ValueError( | ||
f"Dependency group '{included_group}'" | ||
f" (included in '{group}') not found" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): Explicitly raise from a previous error (raise-from-previous-error
)
@@ -335,7 +335,10 @@ | |||
|
|||
@classmethod | |||
def create_from_pep_508( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): Low code quality found in Dependency.create_from_pep_508 - 7% (low-code-quality
)
Explanation
The quality score for this function is below the quality threshold of 25%.This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines. - Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.
Required-by: python-poetry/poetry#10130
Relates-to: python-poetry/poetry#9751
Summary by Sourcery
New Features:
Summary by Sourcery
Implement full PEP 735 support for dependency-groups by parsing the new section, validating its schema, normalizing names, resolving includes with cycle detection, and propagating group membership through dependency creation, accompanied by tests for valid and error cases.
New Features:
Enhancements:
Tests: