fix(uv): resolve per-group dependency versions from lockfile to prevent configuration conflicts#937
fix(uv): resolve per-group dependency versions from lockfile to prevent configuration conflicts#937xangcastle wants to merge 2 commits intomainfrom
Conversation
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.
|
|
This change solved the problem I was having with my dependencies, thanks! |
There was a problem hiding this comment.
💡 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".
| v = preferred_versions.get(pkg_name) | ||
| if v == None: | ||
| v = version_map.get(pkg_name) |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
the comment is pretty obvious
| """ | ||
| result = {} | ||
| for pkg in lock_data.get("package", []): | ||
| if "virtual" not in pkg.get("source", {}): |
There was a problem hiding this comment.
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__") |
There was a problem hiding this comment.
Is it possible we're overwriting [pkg_name] here?


When a
pyproject.tomldeclares dependency groups with overlapping packages at different versions (commonly viatool.uv.conflicts), and some requirements lack explicit version specifiers (e.g."build"), Starlark analysis can fail with:Root Cause
extract_requirement_marker_pairswas falling back tofind_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_versionsinuv/private/extension/projectfile.bzlto read the exact versionsuvalready selected for each dependency group from the lockfile's[package.dev-dependencies]section.collect_activated_extrasnow seedsgroup_preferenceswith these lockfile-resolved versions before parsing requirements, ensuring unconstrained dependencies resolve to the correct per-group version without guessing.Verification
collect_activated_extras_transitive_remap_testintest_projectfile.bzlto assert correct version resolution across conflicting groups.e2e/cases/unconstrained-dependenciestest suite so tests actually execute (added missingpytestdependency, regenerateduv.lock, fixed test imports, and added missingprint_python_package_version.py).15/15 PASSED2/2 PASSED— confirmingpackaging==24.0indepctx_py_main_uvandpackaging==21.3indepctx_py_humble_uvwith no conflicts.