Skip to content
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

fix duplicate packages with multiple conflicting extras declared #11513

Merged
merged 3 commits into from
Feb 18, 2025

Conversation

BurntSushi
Copy link
Member

This implements a somewhat of a cop-out fix for #11479, where the lock
file produced was missing some conflict markers. This in turn could lead
to multiple versions of the same package being installed into the same
environment.

(What follows is one of the commit messages that gets into the weeds
about the specific problem here.)

The particular example I honed in on here was the e3nn -> sympy 1.13.1
and e3nn -> sympy 1.13.3 dependency edges. In particular, while the
former correctly has a conflict marker, the latter's conflict marker was
getting simplified to true. This makes the edges trivially
overlapping, and results in both of them getting installed
simultaneously. (A similar problem happens for the e3nn -> torch
dependency edges.)

Why does this happen? Well, conflict marker simplification works by
detecting which extras are known to be enabled (and disabled) for each
node in the graph. This ends up being expressed as a set of sets, where
each inner set contains items corresponding to "extras is included" or
"extra is excluded."

The logic then is if all of these sets are satisfied by the conflict
marker on the dependency edge, then this conflict marker can be
simplified by assuming all of the inclusions/exclusions to be true.

In this particular case, we run into an issue where the set of
assumptions discovered for e3nn is:

{test[sevennet]}, {}, {~test[m3gnet], ~test[alignn], test[all]}

And the corresponding conflict marker for e3nn -> sympy 1.13.1 is:

extra == 'extra-4-test-all'
or extra == 'extra-4-test-chgnet'
or (extra != 'extra-4-test-alignn' and extra != 'extra-4-test-m3gnet')

And the conflict marker for e3nn -> sympy 1.13.3 is:

extra == 'extra-4-test-alignn' or extra == 'extra-4-test-m3gnet'

Evaluating each of the sets above for sympy 1.13.1's conflict
marker results in them all being true. Simplifying in turn results in
the marker being true. For sympy 1.13.3, not all of the sets are
satisfied, so this marker is not simplified.

I think the fundamental problem here is that our inferences aren't quite
rich enough to make these logical leaps. In particular, the conflict
marker for e3nn -> sympy 1.13.3 is not satisfied by any of our sets.
One might therefore conclude that this dependency edge is impossible.
But! The test[sevennet] set doesn't actually rule out test[m3gnet]
from being included, for example, because there is no conflict. So it is
actually possible for this marker to evaluate to true.

And I think this reveals the problem: for the e3nn -> sympy 1.13.1
conflict marker, the inferences don't capture the fact that
test[sevennet] might have test[m3gnet] enabled, and that would in
turn result in the conflict marker evaluating to false. This directly
implies that our simplification here is inappropriate.

It would be nice to revisit how we build our inferences here so that
they are richer and enable us to make correct logical leaps. For now, we
fix this particular bug with a bit of a cop-out: we skip conflict marker
simplification when there are ambiguous dependency edges.

Fixes #11479

@BurntSushi BurntSushi added bug Something isn't working lock Related to universal resolution and locking labels Feb 14, 2025
@charliermarsh
Copy link
Member

I'm sort of just confirming my understanding, but if we simplified the conflict marker for each set independently, and then checked if each simplification resulted in the same outcome, could we use that?

Or, what if for each set A and B, we did:

A and (extra == 'extra-4-test-all'
or extra == 'extra-4-test-chgnet'
or (extra != 'extra-4-test-alignn' and extra != 'extra-4-test-m3gnet'))
or B and (extra == 'extra-4-test-all'
or extra == 'extra-4-test-chgnet'
or (extra != 'extra-4-test-alignn' and extra != 'extra-4-test-m3gnet'))

Would that be sound?

{ name = "torchvision", version = "0.20.1+cpu", source = { registry = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "torch", version = "2.5.1", source = { registry = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-project-cpu') or (platform_machine != 'aarch64' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124') or (sys_platform == 'darwin' and extra == 'extra-7-project-cpu') or (sys_platform != 'linux' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124')" },
{ name = "torch", version = "2.5.1+cpu", source = { registry = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-project-cpu') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-7-project-cpu') or (sys_platform == 'darwin' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124') or (sys_platform == 'linux' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124')" },
{ name = "torchvision", version = "0.20.1", source = { registry = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-7-project-cpu') or (platform_machine != 'aarch64' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124') or (sys_platform == 'darwin' and extra == 'extra-7-project-cpu') or (sys_platform != 'linux' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124')" },
Copy link
Member

Choose a reason for hiding this comment

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

Is it not still possible for us to simplify out terms like platform_machine != 'aarch64' and extra == 'extra-7-project-cpu' and extra == 'extra-7-project-cu124'?

Copy link
Member Author

@BurntSushi BurntSushi Feb 16, 2025

Choose a reason for hiding this comment

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

Yes I think it's possible. Even before this change, conflict marker simplification does not produce the minimal possible markers in all cases. I think it's just a question of how much time you want me to allocate to the problem. :-)

@BurntSushi
Copy link
Member Author

I'm sort of just confirming my understanding, but if we simplified the conflict marker for each set independently, and then checked if each simplification resulted in the same outcome, could we use that?

If I'm understanding you correctly, I believe that's what the code was already doing (and is still doing for cases where there is no definitive ambiguity). Namely, simplification only happens when evaluating each set results in true. But that still isn't enough here.

I think the problem is that the assumptions inferred from the dependency graph aren't rich enough. They aren't expressing the full set of possibilities.

Verified

This commit was signed with the committer’s verified signature.
BurntSushi Andrew Gallant
The place to look in this snapshot is the `name = "e3nn"` dependency.
Its dependencies on `sympy` and `torch` consist of multiple versions
with overlapping conflict markers. They are getting incorrectly
simplified to `true`.

Verified

This commit was signed with the committer’s verified signature.
BurntSushi Andrew Gallant
The particular example I honed in on here was the `e3nn -> sympy 1.13.1`
and `e3nn -> sympy 1.13.3` dependency edges. In particular, while the
former correctly has a conflict marker, the latter's conflict marker was
getting simplified to `true`. This makes the edges trivially
overlapping, and results in both of them getting installed
simultaneously. (A similar problem happens for the `e3nn -> torch`
dependency edges.)

Why does this happen? Well, conflict marker simplification works by
detecting which extras are known to be enabled (and disabled) for each
node in the graph. This ends up being expressed as a set of sets, where
each inner set contains items corresponding to "extras is included" or
"extra is excluded."

The logic then is if _all_ of these sets are satisfied by the conflict
marker on the dependency edge, then this conflict marker can be
simplified by assuming all of the inclusions/exclusions to be true.

In this particular case, we run into an issue where the set of
assumptions discovered for `e3nn` is:

    {test[sevennet]}, {}, {~test[m3gnet], ~test[alignn], test[all]}

And the corresponding conflict marker for `e3nn -> sympy 1.13.1` is:

    extra == 'extra-4-test-all'
    or extra == 'extra-4-test-chgnet'
    or (extra != 'extra-4-test-alignn' and extra != 'extra-4-test-m3gnet')

And the conflict marker for `e3nn -> sympy 1.13.3` is:

    extra == 'extra-4-test-alignn' or extra == 'extra-4-test-m3gnet'

Evaluating each of the sets above for `sympy 1.13.1`'s conflict
marker results in them all being true. Simplifying in turn results in
the marker being true. For `sympy 1.13.3`, not all of the sets are
satisfied, so this marker is not simplified.

I think the fundamental problem here is that our inferences aren't quite
rich enough to make these logical leaps. In particular, the conflict
marker for `e3nn -> sympy 1.13.3` is not satisfied by _any_ of our sets.
One might therefore conclude that this dependency edge is impossible.
But! The `test[sevennet]` set doesn't actually rule out `test[m3gnet]`
from being included, for example, because there is no conflict. So it is
actually possible for this marker to evaluate to true.

And I think this reveals the problem: for the `e3nn -> sympy 1.13.1`
conflict marker, the inferences don't capture the fact that
`test[sevennet]` _might_ have `test[m3gnet]` enabled, and that would in
turn result in the conflict marker evaluating to `false`. This directly
implies that our simplification here is inappropriate.

It would be nice to revisit how we build our inferences here so that
they are richer and enable us to make correct logical leaps. For now, we
fix this particular bug with a bit of a cop-out: we skip conflict marker
simplification when there are ambiguous dependency edges.

Fixes #11479

Verified

This commit was signed with the committer’s verified signature.
BurntSushi Andrew Gallant
…marker simplification

This is fallout from skipping simplification when two or more edges with
the same package name exist.
@charliermarsh
Copy link
Member

I'm a little worried about how this will affect #11548 and #11559.

@BurntSushi
Copy link
Member Author

I'm a little worried about how this will affect #11548 and #11559.

Yeah hmmm. I think the upside is that this should only apply to cases where there are ambiguous dependency edges (i.e., two different edges with the same package name but different versions). So the lack of simplification I think should be somewhat limited?

@BurntSushi BurntSushi merged commit ed51d76 into main Feb 18, 2025
73 checks passed
@BurntSushi BurntSushi deleted the ag/fix-11479 branch February 18, 2025 12:45
BurntSushi added a commit that referenced this pull request Feb 18, 2025

Verified

This commit was signed with the committer’s verified signature.
BurntSushi Andrew Gallant
The bad merge was a result of merging #11293 and #11513. I think even if
I had only merged the former, it still would have resulted in a bad
merge, since the snapshots hadn't been updated for `provide-extras`
additions.

This fixes the build failures seen here:
https://github.com/astral-sh/uv/actions/runs/13390849790/job/37398029632
BurntSushi added a commit that referenced this pull request Feb 18, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
The bad merge was a result of merging #11293 and #11513. I think even if
I had only merged the former, it still would have resulted in a bad
merge, since the snapshots hadn't been updated for `provide-extras`
additions.

This fixes the build failures seen here:
https://github.com/astral-sh/uv/actions/runs/13390849790/job/37398029632
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Feb 25, 2025
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [astral-sh/uv](https://github.com/astral-sh/uv) | patch | `0.6.0` -> `0.6.3` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>astral-sh/uv (astral-sh/uv)</summary>

### [`v0.6.3`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#063)

[Compare Source](astral-sh/uv@0.6.2...0.6.3)

##### Enhancements

-   Allow quotes around command-line options in `requirement.txt files` ([#&#8203;11644](astral-sh/uv#11644))
-   Initialize PEP 723 script in `uv lock --script` ([#&#8203;11717](astral-sh/uv#11717))

##### Configuration

-   Accept multiple `.env` files in `UV_ENV_FILE` ([#&#8203;11665](astral-sh/uv#11665))

##### Performance

-   Reduce overhead in converting resolutions ([#&#8203;11660](astral-sh/uv#11660))
-   Use `SmallString` on `Hashes` ([#&#8203;11756](astral-sh/uv#11756))
-   Use a `Box` for `Yanked` on `File` ([#&#8203;11755](astral-sh/uv#11755))
-   Use a `SmallString` for the `Yanked` enum ([#&#8203;11715](astral-sh/uv#11715))
-   Use boxed slices for hash vector ([#&#8203;11714](astral-sh/uv#11714))
-   Use install concurrency for bytecode compilation too ([#&#8203;11615](astral-sh/uv#11615))

##### Bug fixes

-   Avoid installing duplicate dependencies across conflicting groups ([#&#8203;11653](astral-sh/uv#11653))
-   Check subdirectory existence after cache heal ([#&#8203;11719](astral-sh/uv#11719))
-   Include uppercase platforms for Windows wheels ([#&#8203;11681](astral-sh/uv#11681))
-   Respect existing PEP 723 script settings in `uv add` ([#&#8203;11716](astral-sh/uv#11716))
-   Reuse refined interpreter to create tool environment ([#&#8203;11680](astral-sh/uv#11680))
-   Skip removed directories during bytecode compilation ([#&#8203;11633](astral-sh/uv#11633))
-   Support conflict markers in `uv export` ([#&#8203;11643](astral-sh/uv#11643))
-   Treat lockfile as outdated if (empty) extras are added ([#&#8203;11702](astral-sh/uv#11702))
-   Display path separators as backslashes on Windows ([#&#8203;11667](astral-sh/uv#11667))
-   Display the built file name instead of the canonicalized name in `uv build` ([#&#8203;11593](astral-sh/uv#11593))
-   Fix message when there are no buildable packages ([#&#8203;11722](astral-sh/uv#11722))
-   Re-allow HTTP schemes for Git dependencies ([#&#8203;11687](astral-sh/uv#11687))

##### Documentation

-   Add anchor links to arguments and options in the CLI reference ([#&#8203;11754](astral-sh/uv#11754))
-   Add link to environment marker specification ([#&#8203;11748](astral-sh/uv#11748))
-   Fix missing a closing bracket in the `cache-keys` setting ([#&#8203;11669](astral-sh/uv#11669))
-   Remove the last edited date from documentation pages ([#&#8203;11753](astral-sh/uv#11753))
-   Fix readme typo ([#&#8203;11742](astral-sh/uv#11742))

### [`v0.6.2`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#062)

[Compare Source](astral-sh/uv@0.6.1...0.6.2)

##### Enhancements

-   Add support for constraining build dependencies with `tool.uv.build-constraint-dependencies` ([#&#8203;11585](astral-sh/uv#11585))
-   Sort dependency group keys when adding new group ([#&#8203;11591](astral-sh/uv#11591))

##### Performance

-   Use an `Arc` for index URLs ([#&#8203;11586](astral-sh/uv#11586))

##### Bug fixes

-   Allow use of x86-64 Python on ARM Windows ([#&#8203;11625](astral-sh/uv#11625))
-   Fix an issue where conflict markers could instigate a very large lock file ([#&#8203;11293](astral-sh/uv#11293))
-   Fix duplicate packages with multiple conflicting extras declared ([#&#8203;11513](astral-sh/uv#11513))
-   Respect color settings for log messages ([#&#8203;11604](astral-sh/uv#11604))
-   Eagerly reject unsupported Git schemes ([#&#8203;11514](astral-sh/uv#11514))

##### Documentation

-   Add documentation for specifying Python versions in tool commands ([#&#8203;11598](astral-sh/uv#11598))

### [`v0.6.1`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#061)

[Compare Source](astral-sh/uv@0.6.0...0.6.1)

##### Enhancements

-   Allow users to mark platforms as "required" for wheel coverage ([#&#8203;10067](astral-sh/uv#10067))
-   Warn for builds in non-build and workspace root pyproject.toml ([#&#8203;11394](astral-sh/uv#11394))

##### Bug fixes

-   Add `--all` to `uvx --reinstall` message ([#&#8203;11535](astral-sh/uv#11535))
-   Fallback to `GET` on HTTP 400 when attempting to use range requests for wheel download ([#&#8203;11539](astral-sh/uv#11539))
-   Prefer local variants in preference selection ([#&#8203;11546](astral-sh/uv#11546))
-   Respect verbatim executable name in `uvx` ([#&#8203;11524](astral-sh/uv#11524))

##### Documentation

-   Add documentation for required environments ([#&#8203;11542](astral-sh/uv#11542))
-   Note that `main.py` used to be `hello.py` ([#&#8203;11519](astral-sh/uv#11519))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNzEuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE3OS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiXX0=-->
loic-lescoat pushed a commit to loic-lescoat/uv that referenced this pull request Mar 2, 2025
The bad merge was a result of merging astral-sh#11293 and astral-sh#11513. I think even if
I had only merged the former, it still would have resulted in a bad
merge, since the snapshots hadn't been updated for `provide-extras`
additions.

This fixes the build failures seen here:
https://github.com/astral-sh/uv/actions/runs/13390849790/job/37398029632
adisbladis added a commit to pyproject-nix/uv2nix that referenced this pull request Mar 5, 2025
As a workaround for pyproject-nix/pyproject.nix#265 so that these "special markers" introduced in astral-sh/uv#11513 won't crash evaluation.

I'm considering these leaking internal extras markers to be a uv bug.
There is no reasonable way to correctly handle this in uv2nix as the generated extras names depends on undocumented uv-specific behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working lock Related to universal resolution and locking
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Duplicate packages with multiple conflicts
2 participants