-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Pass Install Extras to Markers #9553
base: main
Are you sure you want to change the base?
Conversation
I will try to take a closer look at the end of the week. |
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.
This looks really good. Nice work. 👍
It looks like some of the changes are not tested yet or may be redundant. See separate comments for details.
src/poetry/puzzle/provider.py
Outdated
@@ -601,7 +620,9 @@ def complete_package( | |||
|
|||
# For dependency resolution, markers of duplicate dependencies must be | |||
# mutually exclusive. | |||
active_extras = None if package.is_root() else dependency.extras | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
Seems like that is not tested yet. (Tests succeed without this change.) You probably have to add a test similar to test_solver_resolves_duplicate_dependency_in_extra
, which tests this for non root extras.
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.
Ah yep, added test_solver_resolves_duplicate_dependency_in_root_extra()
. But this results in both versions of A being selected, see the note about the bug here
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.
Oops, I simply forgot to populate active_extras
when constructing the Solver
🤦
I've fixed and validated that this adds test coverage for this change! However I see your comment about test_solver_resolves_duplicate_dependency_in_root_extra()
relying on root extra markers being an unrealistic situation. I'd lean towards leaving this as is but happy to replace that test and remove this change if they're unnecessary.
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.
I added another comment concerning the test. I think we can keep the test with slight modifications.
src/poetry/puzzle/provider.py
Outdated
@@ -541,7 +555,12 @@ def complete_package( | |||
if dep.name in self.UNSAFE_PACKAGES: | |||
continue | |||
|
|||
if self._env and not dep.marker.validate(self._env.marker_env): | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
Seems like the non root part is not tested yet. (Tests succeed when replacing dependency.extras
with None
.) Maybe, this path can be triggered by taking one of your tests and introducing an intermediate package?
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.
Great catch! While trying to add a test for this I uncovered an issue referenced in a top-level comment. The breaking test I've added doesn't include the intermediate package for simplicity but I can add it as we get to the bottom of that.
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.
You can ignore the comment above, that breaking test relied on conflicting versions of the same package between extras which I don't think is a realistic situation that needs to be covered.
Instead, what I've discovered is that this functionality is already covered: markers are used to populate the Dependency.in_extras field which is then used to skip unneeded packages just below for non-root packages. This is described as the more typical behavior for non-root extras in this comment since root extras aren't passed as marker values into dependencies.
So I've updated this accordingly, to only use self._active_root_extras
to construct marker values in the root case while leaving the non-root case as simply self._env.marker_env
.
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.
that breaking test relied on conflicting versions of the same package between extras which I don't think is a realistic situation that needs to be covered.
Not sure what I was thinking here, that's absolutely an important case in transitive dependencies as well as root (e.g. what if your dependency has a --cuda
flag to switch Pytorch versions, and you want your own --cuda
flag to select between the conflicting cpu and cuda versions in the transitive Pytorch dependency).
This was in fact the bug identified earlier. I added a better test based on the earlier one to illustrate it, which covers conflicting dependencies in root and non-root cases. It would pass in the root case (due to duplicate resolution) but fail in the non-root case.
I then pushed a fix in the latest commit, all tests pass but could use validation that it's not unwise: I updated the duplicate resolution logic to raise OverrideNeededError
s when duplicates conflict even with different extras, but not perform overlapping marker resolution in those "same package with different extras" cases.
I found that in that case overlapping marker resolution results in incomplete lockfiles which don't contain all possible package versions, while the override exceptions are needed to prevent solver failure.
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.
I found that in that case overlapping marker resolution results in incomplete lockfiles which don't contain all possible package versions, while the override exceptions are needed to prevent solver failure.
Yes, overlapping marker resolution is not prepared for duplicates with different extras.
I updated the duplicate resolution logic to raise
OverrideNeededError
s when duplicates conflict even with different extras, but not perform overlapping marker resolution in those "same package with different extras" cases.
Unfortunately, it is not that simple. The updated logic will only produce correct results if markers are already mutually exclusive. We have to make sure that overrides are not triggered if markers do overlap. I just added a commit with a refined logic. Please take a look.
and ( | ||
not self._env | ||
or dep.marker.validate( | ||
self._marker_values( | ||
self._active_root_extras | ||
if dependency_package.package.is_root() | ||
else dependency_package.dependency.extras | ||
) | ||
) | ||
) |
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.
It looks like this change is not required by any test. To be honest, I have currently no idea if this change is redundant or if a test is missing...
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.
Ah yeah I wasn't sure what the best policy was here: I just updated the places where I saw markers being evaluated to include extras. What I found is that of the three changes in provider.py
, any one of them is sufficient to pass the first round of tests. So I think there is some redundancy, but my thought is to not eliminate anything yet due to the issue with uncovered around not resolving conflicts with their own different extras.
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.
Confirmed that this change is redundant: either this change or the one on lines 558 are sufficient. Happy to leave this in or to remove it since we haven't found a case depending on this case alone.
In the one or two cases I looked at the other change alone took the same number of steps as both changes together, while this change alone took more steps. But I'm not sure if there are some cases I didn't find on a glance where having both changes may reduce the number of steps required.
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.
I took a closer look so that I am quite sure now that we can remove the complete filtering statement (_dependencies = [...]
) because incompatibilities_for()
is only called after complete_package()
, which does the same filtering, so that _dependencies
and dependencies
will always be equal. I even think we should remove it considering how long it took us to check that it is redundant. Otherwise, it will remain a source of confusion.
Thank you so much for the great feedback here! All the comments make sense to me, I'm working through it and will submit updates and responses later this week |
How would this differentiate between the
poetry install -E cuda124 and define the in the package [tool.poetry.extras]
cuda124 = ["torch"] Here your suggestion would add [tool.poetry.dependencies]
python = "^3.10"
torch = [
{ markers = "extra == 'cuda124'", version = "2.4.0+cu124", optional = true},
^^^^^^
poetry add "pandas[aws]" which would show in the [tool.poetry.dependencies]
python = "^3.11"
pandas = {extras = ["aws"], version = "^2.2.2"}
^^^^^ Would the differentiation be made on the following?
If so, I must have misunderstood the docs because I thought these 2 syntaces express the same requirements/constraints |
I apologize for the long delay here, I had to pause this before I managed to finish all the updates. But it hasn't left my mind, and I'll be able to resume the updates in a couple weeks and will push them up and respond to everything in mid-September! Thanks for the patience |
@reesehyde Any update on this? Would love to have this feature available. ^^ |
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.
I apologize for the long delay here, I've been working on the suggested updates but uncovered an issue in the process. I've added a couple breaking tests which will hopefully help illuminate it. But I'm a little stuck on the best course of action, so would love a push in the right direction if you're able @radoering.
It seems that part of the reason the initial tests passed is because they used conflicting versions of torch
, which works only because they then go through the process of merging duplicates since the package name is the same in all cases.
However, dependencies with their own differing extras (e.g. torch[cpu]
and torch[cuda]
) are not considered duplicates, so they result in a SolverProblemError
.
Simply considering them duplicates creates its own issues (e.g. see the test_solver_resolves_duplicate_dependency_in_extra test). So we need some way to do overlapping marker resolution in these cases without breaking other solving cases.
Another possibility would be scoping this PR down to not support conflicting dependency extras, but I'm not certain how that would look.
src/poetry/puzzle/provider.py
Outdated
@@ -601,7 +620,9 @@ def complete_package( | |||
|
|||
# For dependency resolution, markers of duplicate dependencies must be | |||
# mutually exclusive. | |||
active_extras = None if package.is_root() else dependency.extras | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
Ah yep, added test_solver_resolves_duplicate_dependency_in_root_extra()
. But this results in both versions of A being selected, see the note about the bug here
and ( | ||
not self._env | ||
or dep.marker.validate( | ||
self._marker_values( | ||
self._active_root_extras | ||
if dependency_package.package.is_root() | ||
else dependency_package.dependency.extras | ||
) | ||
) | ||
) |
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.
Ah yeah I wasn't sure what the best policy was here: I just updated the places where I saw markers being evaluated to include extras. What I found is that of the three changes in provider.py
, any one of them is sufficient to pass the first round of tests. So I think there is some redundancy, but my thought is to not eliminate anything yet due to the issue with uncovered around not resolving conflicts with their own different extras.
src/poetry/puzzle/provider.py
Outdated
@@ -541,7 +555,12 @@ def complete_package( | |||
if dep.name in self.UNSAFE_PACKAGES: | |||
continue | |||
|
|||
if self._env and not dep.marker.validate(self._env.marker_env): | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
Great catch! While trying to add a test for this I uncovered an issue referenced in a top-level comment. The breaking test I've added doesn't include the intermediate package for simplicity but I can add it as we get to the bottom of that.
@GeeCastro this is a great question, you're right that they are basically the same syntax! The problem comes up when you have conflicting extras, e.g. if the |
… with active root extras
4734c0f
to
ad85d88
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.
@radoering thank you again for all the help here! I've pushed updates, responded to all threads, and rebased on main
.
I believe everything has test coverage now, there are just small open questions on:
- Whether to leave redundant changes in both places or isolate to one place
- Either the L465 change or L558 change are sufficient to pass the new
test_installer
tests, but not sure if it's worth including both for "marker value correctness" or in cases where having both may shorten the solving steps required.
- Either the L465 change or L558 change are sufficient to pass the new
- Whether to leave or remove the changes and test which rely on root extra dependencies having the extra marker value even though it's not generally populated in those cases
- I added test coverage for the L623 change but you mentioned that test scenario may be unrealistic
Edit: The bug referenced was still present, but I added an improved test for it and finally found a fix. Final open question is:
- A review of that new logic
src/poetry/puzzle/provider.py
Outdated
@@ -541,7 +555,12 @@ def complete_package( | |||
if dep.name in self.UNSAFE_PACKAGES: | |||
continue | |||
|
|||
if self._env and not dep.marker.validate(self._env.marker_env): | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
You can ignore the comment above, that breaking test relied on conflicting versions of the same package between extras which I don't think is a realistic situation that needs to be covered.
Instead, what I've discovered is that this functionality is already covered: markers are used to populate the Dependency.in_extras field which is then used to skip unneeded packages just below for non-root packages. This is described as the more typical behavior for non-root extras in this comment since root extras aren't passed as marker values into dependencies.
So I've updated this accordingly, to only use self._active_root_extras
to construct marker values in the root case while leaving the non-root case as simply self._env.marker_env
.
src/poetry/puzzle/provider.py
Outdated
@@ -601,7 +620,9 @@ def complete_package( | |||
|
|||
# For dependency resolution, markers of duplicate dependencies must be | |||
# mutually exclusive. | |||
active_extras = None if package.is_root() else dependency.extras | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
Oops, I simply forgot to populate active_extras
when constructing the Solver
🤦
I've fixed and validated that this adds test coverage for this change! However I see your comment about test_solver_resolves_duplicate_dependency_in_root_extra()
relying on root extra markers being an unrealistic situation. I'd lean towards leaving this as is but happy to replace that test and remove this change if they're unnecessary.
and ( | ||
not self._env | ||
or dep.marker.validate( | ||
self._marker_values( | ||
self._active_root_extras | ||
if dependency_package.package.is_root() | ||
else dependency_package.dependency.extras | ||
) | ||
) | ||
) |
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.
Confirmed that this change is redundant: either this change or the one on lines 558 are sufficient. Happy to leave this in or to remove it since we haven't found a case depending on this case alone.
In the one or two cases I looked at the other change alone took the same number of steps as both changes together, while this change alone took more steps. But I'm not sure if there are some cases I didn't find on a glance where having both changes may reduce the number of steps required.
pre-commit.ci autofix |
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.
I think we are slowly approaching the finish line. 😄
I addressed all open questions in the relevant threads.
solver = Solver(package, pool, [], [], io, active_root_extras=extra) | ||
transaction = solver.solve() |
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.
solver = Solver(package, pool, [], [], io, active_root_extras=extra) | |
transaction = solver.solve() | |
solver = Solver( | |
package, pool, [], [package_a1, package_a2], io, active_root_extras=extra | |
) | |
with solver.use_environment(MockEnv()): | |
transaction = solver.solve() |
More realistic: Solver will only be called with active_root_extras
for installation (not for creating the lock file). In this case, locked
will be populated. Further, we are solving for a specific environment.
If we did not solve for a specific environment, the expectation would have to be different.
@@ -0,0 +1,24 @@ | |||
[[package]] |
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.
This file seems to be unused.
and ( | ||
not self._env | ||
or dep.marker.validate( | ||
self._marker_values( | ||
self._active_root_extras | ||
if dependency_package.package.is_root() | ||
else dependency_package.dependency.extras | ||
) | ||
) | ||
) |
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.
I took a closer look so that I am quite sure now that we can remove the complete filtering statement (_dependencies = [...]
) because incompatibilities_for()
is only called after complete_package()
, which does the same filtering, so that _dependencies
and dependencies
will always be equal. I even think we should remove it considering how long it took us to check that it is redundant. Otherwise, it will remain a source of confusion.
src/poetry/puzzle/provider.py
Outdated
@@ -541,7 +555,12 @@ def complete_package( | |||
if dep.name in self.UNSAFE_PACKAGES: | |||
continue | |||
|
|||
if self._env and not dep.marker.validate(self._env.marker_env): | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
I found that in that case overlapping marker resolution results in incomplete lockfiles which don't contain all possible package versions, while the override exceptions are needed to prevent solver failure.
Yes, overlapping marker resolution is not prepared for duplicates with different extras.
I updated the duplicate resolution logic to raise
OverrideNeededError
s when duplicates conflict even with different extras, but not perform overlapping marker resolution in those "same package with different extras" cases.
Unfortunately, it is not that simple. The updated logic will only produce correct results if markers are already mutually exclusive. We have to make sure that overrides are not triggered if markers do overlap. I just added a commit with a refined logic. Please take a look.
src/poetry/puzzle/provider.py
Outdated
@@ -601,7 +620,9 @@ def complete_package( | |||
|
|||
# For dependency resolution, markers of duplicate dependencies must be | |||
# mutually exclusive. | |||
active_extras = None if package.is_root() else dependency.extras | |||
active_extras = ( | |||
self._active_root_extras if package.is_root() else dependency.extras |
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.
I added another comment concerning the test. I think we can keep the test with slight modifications.
Pull Request Check List
Resolves:
poetry install
, but respected inpip install
#7748This PR exposes the
extras
supplied at install to the 'extra' marker for dependencies, taking up @radoering's offer to create a PR implementing this functionality to allow switching torch installation versions based on acuda
extra.It is a duplicate of the feature in python-poetry/poetry-core#613 but it appears that PR is no longer active, with the last updates in August 2023 and an unanswered question about timeline from March 2024. This PR takes a different approach: I noticed that python-poetry/poetry-core#636 added special handling for the
extras
marker value but that marker isn't populated often, so I opted to populate the value when extras are specified in the installer.See issues and unit tests for more details but the idea is to, among other things, enable exclusive extras like so:
@radoering it looks like you're very familiar with this issue, would you be the right person to review? 🙏
I'm sure I'm missing something here but look forward to iterating!
Edit by @radoering: fix links to poetry-core PRs