Skip to content

fix(uv): resolve per-group dependency versions from lockfile to prevent configuration conflicts#937

Open
xangcastle wants to merge 2 commits intomainfrom
xangcastle/unconstrained-dependencies-conflits
Open

fix(uv): resolve per-group dependency versions from lockfile to prevent configuration conflicts#937
xangcastle wants to merge 2 commits intomainfrom
xangcastle/unconstrained-dependencies-conflits

Conversation

@xangcastle
Copy link
Copy Markdown
Member

@xangcastle xangcastle commented Apr 15, 2026

When a pyproject.toml declares dependency groups with overlapping packages at different versions (commonly via tool.uv.conflicts), and some requirements lack explicit version specifiers (e.g. "build"), Starlark analysis can fail with:

Configuration conflict! Package packaging specifies two or more default package states!

Root Cause

extract_requirement_marker_pairs was falling back to find_matching_version(">=0", ...) for unconstrained requirements, which always picks the highest version across the entire lockfile. This meant both dependency groups could end up resolving the same high version (e.g. build==1.4.3), pulling in transitive dependencies (e.g. packaging==24.0) that conflicted with the group's explicit pins (e.g. packaging==21.3).

Fix

Introduce _extract_lockfile_group_versions in uv/private/extension/projectfile.bzl to read the exact versions uv already selected for each dependency group from the lockfile's [package.dev-dependencies] section. collect_activated_extras now seeds group_preferences with these lockfile-resolved versions before parsing requirements, ensuring unconstrained dependencies resolve to the correct per-group version without guessing.

Verification

  • Added collect_activated_extras_transitive_remap_test in test_projectfile.bzl to assert correct version resolution across conflicting groups.
  • Fixed the e2e/cases/unconstrained-dependencies test suite so tests actually execute (added missing pytest dependency, regenerated uv.lock, fixed test imports, and added missing print_python_package_version.py).
  • Unit tests: 15/15 PASSED
  • E2E tests: 2/2 PASSED — confirming packaging==24.0 in depctx_py_main_uv and packaging==21.3 in depctx_py_humble_uv with no conflicts.

Fix unconstrained dependency resolution when a lockfile contains multiple
versions of the same package across different dependency groups. Previously,
transitive dependencies were not correctly remapped to match the version
preferred by their group's direct dependencies, causing version mismatches
and build failures in multi-context Python projects.

The fix extracts resolved package versions per group from the lockfile's
`dev-dependencies` section and uses them as preferred versions during both
direct and transitive dependency resolution. This ensures that each
dependency group consistently uses the versions recorded in the lockfile.

Also updates the `e2e/cases/unconstrained-dependencies` test case to use
`aspect_rules_py` instead of custom `py_*` rules, fixes test imports and
assertions, and adds `pytest` as an explicit dependency.
@aspect-workflows
Copy link
Copy Markdown

aspect-workflows bot commented Apr 15, 2026

Bazel 8 (Test)

1 test target passed

Targets
//uv/private/extension:extract_requirement_marker_pairs_tests_test_6 [k8-fastbuild]                 95ms

Total test execution time was 95ms. 119 tests (99.2%) were fully cached saving 54s.


Bazel 9 (Test)

1 test target passed

Targets
//uv/private/extension:extract_requirement_marker_pairs_tests_test_6 [k8-fastbuild]                 77ms

Total test execution time was 77ms. 118 tests (99.2%) were fully cached saving 1m.


Bazel 8 (Test)

e2e

2 test targets passed

Targets
//cases/unconstrained-dependencies:get_python_package_version_humble_test [k8-fastbuild-ST-7cf6bca30bef]658ms
//cases/unconstrained-dependencies:get_python_package_version_main_test [k8-fastbuild-ST-7b13bbdb9325]2s

Total test execution time was 3s. 52 tests (96.3%) were fully cached saving 45s.


Bazel 9 (Test)

e2e

2 test targets passed

Targets
//cases/unconstrained-dependencies:get_python_package_version_humble_test [k8-fastbuild-ST-7cf6bca30bef]662ms
//cases/unconstrained-dependencies:get_python_package_version_main_test [k8-fastbuild-ST-7b13bbdb9325]1s

Total test execution time was 2s. 52 tests (96.3%) were fully cached saving 42s.


Bazel 8 (Test)

examples/uv_pip_compile

All tests were cache hits

1 test (100.0%) was fully cached saving 444ms.


Buildifier

@xangcastle xangcastle marked this pull request as ready for review April 15, 2026 18:17
@xangcastle xangcastle changed the title unconstrained dependencies conflicts fix(uv): resolve per-group dependency versions from lockfile to prevent configuration conflicts Apr 15, 2026
@mteggia-bdai
Copy link
Copy Markdown

This change solved the problem I was having with my dependencies, thanks!

@xangcastle xangcastle enabled auto-merge (squash) April 15, 2026 18:39
@jbedard
Copy link
Copy Markdown
Member

jbedard commented Apr 16, 2026

@codex

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9ad57d734

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +83 to +85
v = preferred_versions.get(pkg_name)
if v == None:
v = version_map.get(pkg_name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor specifier when resolving preferred package versions

extract_requirement_marker_pairs now returns preferred_versions[pkg_name] before evaluating the current requirement's specifier, and collect_activated_extras populates that preference map from earlier requirements in the same group. If a group contains the same package more than once with different specifiers/markers (for example, marker-split constraints), later parses will ignore the active specifier and force the cached version, producing an incorrect dependency graph for that configuration.

Useful? React with 👍 / 👎.

@@ -0,0 +1,4 @@
from get_python_package_version import get_version
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Import the correct helper in version-printing script

This script imports get_version, but get_python_package_version.py defines get_packaging_version. Running either new py_binary target will fail at import time with ImportError, so the added executable targets in this e2e case are currently broken.

Useful? React with 👍 / 👎.

visited[it] = 1

for next, markers in graph.get(it, {}).items():
# Convert `next`, being a dependency potentially with marker, to its base package
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments were deleted?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment is pretty obvious

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not lol

"""
result = {}
for pkg in lock_data.get("package", []):
if "virtual" not in pkg.get("source", {}):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? Why would packages with "virtual" sources not have group versions?

for dep in deps:
pkg_name = normalize_name(dep["name"])
if "version" in dep:
result.setdefault(group_name, {})[pkg_name] = (lock_id, pkg_name, dep["version"], "__base__")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible we're overwriting [pkg_name] here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants