-
Notifications
You must be signed in to change notification settings - Fork 872
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
Multiple conflicting extras installed when using explicit index assignments #9289
Comments
Interesting, thanks. I don't think the extra indexes are relevant, since you can reproduce with: [project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11,<3.12"
dependencies = []
[project.optional-dependencies]
foo = [
"idna==3.10",
]
bar = [
"idna==3.9",
]
baz = [
"anyio",
]
[tool.uv]
conflicts = [
[
{ extra = "foo" },
{ extra = "bar" },
],
] From there,
|
Thanks. It also happens if the two conflicting extra groups share the common dependant: [project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11,<3.12"
dependencies = []
[project.optional-dependencies]
foo = [
"idna==3.10",
"anyio",
]
bar = [
"idna==3.9",
"anyio",
]
[tool.uv]
conflicts = [
[
{ extra = "foo" },
{ extra = "bar" },
],
] |
I think i see a path to fixing this. I think it has two components:
One hiccup here is that this doesn't take groups into account, so we might not be able to reuse markers for this. We'll instead need an additional field to encode this extra/group inclusion logic. |
Working it out by hand, I think the conditional logic for each idna dependency in each of the three forks present in this example is:
Which combines to:
And simplifies to:
Note that because of how extras work, the above notation is quite misleading. It's more clearly written like this:
|
When we generate conflict markers for each resolution after the resolver runs, it turns out that generating them just from exclusion rules is not sufficient. For example, if `foo` and `bar` are declared as conflicting extras, then we end up with the following forks: A: extra != 'foo' B: extra != 'bar' C: extra != 'foo' and extra != 'bar' Now let's take an example where these forks don't share the same version for all packages. Consider a case where `idna==3.9` is in forks A and C, but where `idna==3.10` is in fork B. If we combine the markers in forks A and C through disjunction, we get the following: idna==3.9: extra != 'foo' or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' But these are clearly not disjoint. Both dependencies could be selected, for example, when neither `foo` nor `bar` are active. We can remedy this by keeping around the inclusion rules for each fork: A: extra != 'foo' and extra == 'bar' B: extra != 'bar' and extra == 'foo' C: extra != 'foo' and extra != 'bar' And so for `idna`, we have: idna==3.9: (extra != 'foo' and extra == 'bar') or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' and extra == 'foo' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' and extra == 'foo' And these *are* properly disjoint. There is no way for them both to be active. This also correctly accounts for fork C where neither `foo` nor `bar` are active, and yet, `idna==3.9` is still enabled but `idna==3.10` is not. (In the [motivating example], this comes from `baz` being enabled.) That is, this captures the idea that for `idna==3.10` to be installed, there must actually be a specific extra that is enabled. That's what makes it disjoint from `idna==3.9`. We aren't quite done yet, because this does add *too many* conflict markers to dependency edges that don't need it. In the next commit, we'll add in our world knowledge to simplify these conflict markers. [motivating example]: #9289
When we generate conflict markers for each resolution after the resolver runs, it turns out that generating them just from exclusion rules is not sufficient. For example, if `foo` and `bar` are declared as conflicting extras, then we end up with the following forks: A: extra != 'foo' B: extra != 'bar' C: extra != 'foo' and extra != 'bar' Now let's take an example where these forks don't share the same version for all packages. Consider a case where `idna==3.9` is in forks A and C, but where `idna==3.10` is in fork B. If we combine the markers in forks A and C through disjunction, we get the following: idna==3.9: extra != 'foo' or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' But these are clearly not disjoint. Both dependencies could be selected, for example, when neither `foo` nor `bar` are active. We can remedy this by keeping around the inclusion rules for each fork: A: extra != 'foo' and extra == 'bar' B: extra != 'bar' and extra == 'foo' C: extra != 'foo' and extra != 'bar' And so for `idna`, we have: idna==3.9: (extra != 'foo' and extra == 'bar') or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' and extra == 'foo' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' and extra == 'foo' And these *are* properly disjoint. There is no way for them both to be active. This also correctly accounts for fork C where neither `foo` nor `bar` are active, and yet, `idna==3.9` is still enabled but `idna==3.10` is not. (In the [motivating example], this comes from `baz` being enabled.) That is, this captures the idea that for `idna==3.10` to be installed, there must actually be a specific extra that is enabled. That's what makes it disjoint from `idna==3.9`. We aren't quite done yet, because this does add *too many* conflict markers to dependency edges that don't need it. In the next commit, we'll add in our world knowledge to simplify these conflict markers. [motivating example]: #9289
When we generate conflict markers for each resolution after the resolver runs, it turns out that generating them just from exclusion rules is not sufficient. For example, if `foo` and `bar` are declared as conflicting extras, then we end up with the following forks: A: extra != 'foo' B: extra != 'bar' C: extra != 'foo' and extra != 'bar' Now let's take an example where these forks don't share the same version for all packages. Consider a case where `idna==3.9` is in forks A and C, but where `idna==3.10` is in fork B. If we combine the markers in forks A and C through disjunction, we get the following: idna==3.9: extra != 'foo' or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' But these are clearly not disjoint. Both dependencies could be selected, for example, when neither `foo` nor `bar` are active. We can remedy this by keeping around the inclusion rules for each fork: A: extra != 'foo' and extra == 'bar' B: extra != 'bar' and extra == 'foo' C: extra != 'foo' and extra != 'bar' And so for `idna`, we have: idna==3.9: (extra != 'foo' and extra == 'bar') or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' and extra == 'foo' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' and extra == 'foo' And these *are* properly disjoint. There is no way for them both to be active. This also correctly accounts for fork C where neither `foo` nor `bar` are active, and yet, `idna==3.9` is still enabled but `idna==3.10` is not. (In the [motivating example], this comes from `baz` being enabled.) That is, this captures the idea that for `idna==3.10` to be installed, there must actually be a specific extra that is enabled. That's what makes it disjoint from `idna==3.9`. We aren't quite done yet, because this does add *too many* conflict markers to dependency edges that don't need it. In the next commit, we'll add in our world knowledge to simplify these conflict markers. [motivating example]: #9289
When we generate conflict markers for each resolution after the resolver runs, it turns out that generating them just from exclusion rules is not sufficient. For example, if `foo` and `bar` are declared as conflicting extras, then we end up with the following forks: A: extra != 'foo' B: extra != 'bar' C: extra != 'foo' and extra != 'bar' Now let's take an example where these forks don't share the same version for all packages. Consider a case where `idna==3.9` is in forks A and C, but where `idna==3.10` is in fork B. If we combine the markers in forks A and C through disjunction, we get the following: idna==3.9: extra != 'foo' or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' But these are clearly not disjoint. Both dependencies could be selected, for example, when neither `foo` nor `bar` are active. We can remedy this by keeping around the inclusion rules for each fork: A: extra != 'foo' and extra == 'bar' B: extra != 'bar' and extra == 'foo' C: extra != 'foo' and extra != 'bar' And so for `idna`, we have: idna==3.9: (extra != 'foo' and extra == 'bar') or (extra != 'foo' and extra != 'bar') idna==3.10: extra != 'bar' and extra == 'foo' Which simplifies to: idna==3.9: extra != 'foo' idna==3.10: extra != 'bar' and extra == 'foo' And these *are* properly disjoint. There is no way for them both to be active. This also correctly accounts for fork C where neither `foo` nor `bar` are active, and yet, `idna==3.9` is still enabled but `idna==3.10` is not. (In the [motivating example], this comes from `baz` being enabled.) That is, this captures the idea that for `idna==3.10` to be installed, there must actually be a specific extra that is enabled. That's what makes it disjoint from `idna==3.9`. We aren't quite done yet, because this does add *too many* conflict markers to dependency edges that don't need it. In the next commit, we'll add in our world knowledge to simplify these conflict markers. [motivating example]: #9289
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
In the course of working on #9289, I've had to devise some additions to our markers. While we are still staying strictly compatible with the PEP 508 format, we will be abusing the `extra` expression to carry a lot more information. Specifically, we want the following additional operations: * Simplify `extra != 'foo'` * Remove all extra expressions * Remove everything except extra expressions My work on #9289 requires all of these (which will be in a future in PR).
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
Some of these are regression tests or related regression tests for #9289. Others are regression tests for bugs I found while working on #9289. Most of the outputs for these tests are wrong. For example, some tests are asserting that multiple versions of the same package are installed. But we add them now before our fixes so that we can see how they change.
This fixes a bug found by Konsti[1] where not all extras would propagate correctly. This could lead to improper simplification steps. The fix here is to revisit nodes in the graph unless no changes have been made to the set of enabled extras. We also add a regression test whose snapshot changes without this fix. I tried writing a test case by hand but couldn't figure it out. The original pytorch MRE in #9289 also has a different lock file with this fix, but we really shouldn't be adding more pytorch tests given how beefy they are. So I found this case using airflow, which is also beefy, but hopefully good enough. [1]: #9370 (comment)
This fixes a bug found by Konsti[1] where not all extras would propagate correctly. This could lead to improper simplification steps. The fix here is to revisit nodes in the graph unless no changes have been made to the set of enabled extras. We also add a regression test whose snapshot changes without this fix. I tried writing a test case by hand but couldn't figure it out. The original pytorch MRE in #9289 also has a different lock file with this fix, but we really shouldn't be adding more pytorch tests given how beefy they are. So I found this case using airflow, which is also beefy, but hopefully good enough. [1]: #9370 (comment)
This PR adds a notion of "conflict markers" to the lock file as an attempt to address #9289. The idea is to encode a new kind of boolean expression indicating how to choose dependencies based on which extras are activated. As an example of what conflict markers look like, consider one of the cases brought up in #9289, where `anyio` had unconditional dependencies on two different versions of `idna`. Now, those are gated by markers, like this: ```toml [[package]] name = "anyio" version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-project-foo'" }, { name = "idna", version = "3.6", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-7-project-bar' or extra != 'extra-7-project-foo'" }, { name = "sniffio" }, ] ``` The odd extra values like `extra-7-project-foo` are an encoding of not just the conflicting extra (`foo`) but also the package it's declared for (`project`). We need both bits of information because different packages may have the same extra name, even if they are completely unrelated. The `extra-` part is a prefix to distinguish it from groups (which, in this case, would be encoded as `group-7-project-foo` if `foo` were a dependency group). And the `7` part indicates the length of the package name which makes it possible to parse out the package and extra name from this encoding. (We don't actually utilize that property, but it seems like good sense to do it in case we do need to extra information from these markers.) While this preserves PEP 508 compatibility at a surface level, it does require utilizing this encoding scheme in order to evaluate them when they're present (which only occurs when conflicting extras/groups are declared). My sense is that the most complex part of this change is not just adding conflict markers, but their simplification. I tried to address this in the code comments and commit messages. Reviewers should look at this commit-by-commit. Fixes #9289, Fixes #9546, Fixes #9640, Fixes #9622, Fixes #9498, Fixes #9701, Fixes #9734
When conflicting extra groups share a dependant, both extra groups seem to be installed.
Consider the following example adapted from the docs:
The key difference is the additional extra
metrics
that includes thetorchmetrics
package that depends ontorch
.When I lock and sync both cpu and metrics extras, I get multiple versions of torch installed:
The text was updated successfully, but these errors were encountered: