-
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
… with active root extras
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.
@@ -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
@@ -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.
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.
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.
@@ -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.
@@ -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 |
@pytest.mark.parametrize("with_extra", [False, True]) | ||
def test_solver_resolves_duplicate_dependency_in_root_extra( | ||
package: ProjectPackage, | ||
pool: RepositoryPool, | ||
repo: Repository, | ||
io: NullIO, | ||
with_extra: bool, | ||
) -> None: | ||
""" | ||
Without extras, a newer version of A can be chosen than with 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.
This is only a theoretical issue. In other words, the setup of the test cannot happen when invoking poetry.
First of all, you cannot create this issue with Poetry 1.8 because if you try something like:
[tool.poetry.dependencies]
python = "^3.9"
tomli = [
{ version = ">=1" },
{ version = "^1", optional = true },
]
[tool.poetry.extras]
foo = [ "tomli" ]
... both dependencies are in the extra. However, with the main branch (by the way, please rebase your branch when it suits you) and PEP 621 support you can define:
[project]
# ...
requires-python = ">=3.9"
dependencies = [
"tomli>=1",
]
[project.optional-dependencies]
foo = [
"tomli>=1,<2",
]
When you debug this, you will notice that the tomli dependency from the extra has no extra == "foo"
marker. Generally speaking, dependencies of root extras do not have the extra in the marker but only the attribute _in_extras
set. Thus, a more realistic test will look like that (and does not fail):
def test_solver_resolves_duplicate_dependency_in_root_extra(
package: ProjectPackage,
pool: RepositoryPool,
repo: Repository,
io: NullIO,
) -> None:
"""
Without extras, a newer version of A could be chosen than with root extras.
However, only the older version is chosen.
"""
package_a1 = get_package("A", "1.0")
package_a2 = get_package("A", "2.0")
dep = get_dependency("A", ">=1.0")
package.add_dependency(dep)
dep_extra = get_dependency("A", "^1.0", optional=True)
dep_extra._in_extras = ["foo"]
package.extras = {canonicalize_name("foo"): [dep_extra]}
package.add_dependency(dep_extra)
repo.add_package(package_a1)
repo.add_package(package_a2)
solver = Solver(package, pool, [], [], io)
transaction = solver.solve()
check_solver_result(transaction, ([{"job": "install", "package": package_a1}]))
I only checked this test for now and will take a look at the installer tests later.
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