diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 0be0bf04220..0b581eaf877 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -2,6 +2,7 @@ import time +from contextlib import suppress from typing import TYPE_CHECKING from poetry.core.packages.dependency import Dependency @@ -365,12 +366,21 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]: ) return dependency.complete_name - try: - version = packages[0] - except IndexError: - version = None + package = None + if dependency.name not in self._use_latest: + # prefer locked version of compatible (not exact same) dependency; + # required in order to not unnecessarily update dependencies with + # extras, e.g. "coverage" vs. "coverage[toml]" + locked = self._locked.get(dependency.name, None) + if locked is not None: + package = next( + (p for p in packages if p.version == locked.version), None + ) + if package is None: + with suppress(IndexError): + package = packages[0] - if version is None: + if package is None: # If there are no versions that satisfy the constraint, # add an incompatibility that indicates that. self._add_incompatibility( @@ -379,12 +389,12 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]: return dependency.complete_name else: - version = locked + package = locked - version = self._provider.complete_package(version) + package = self._provider.complete_package(package) conflict = False - for incompatibility in self._provider.incompatibilities_for(version): + for incompatibility in self._provider.incompatibilities_for(package): self._add_incompatibility(incompatibility) # If an incompatibility is already satisfied, then selecting version @@ -399,9 +409,9 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]: ) if not conflict: - self._solution.decide(version) + self._solution.decide(package) self._log( - f"selecting {version.complete_name} ({version.full_pretty_version})" + f"selecting {package.complete_name} ({package.full_pretty_version})" ) return dependency.complete_name diff --git a/tests/mixology/version_solver/test_with_lock.py b/tests/mixology/version_solver/test_with_lock.py index 42b8d77fb53..cc9e28f12d6 100644 --- a/tests/mixology/version_solver/test_with_lock.py +++ b/tests/mixology/version_solver/test_with_lock.py @@ -136,3 +136,35 @@ def test_with_compatible_locked_dependencies_use_latest( }, use_latest=["foo"], ) + + +def test_with_compatible_locked_dependencies_with_extras( + root: ProjectPackage, provider: Provider, repo: Repository +): + root.add_dependency(Factory.create_dependency("foo", "^1.0")) + + package_foo_0 = get_package("foo", "1.0.0") + package_foo_1 = get_package("foo", "1.0.1") + bar_extra_dep = Factory.create_dependency( + "bar", {"version": "^1.0", "extras": "extra"} + ) + for package_foo in (package_foo_0, package_foo_1): + package_foo.add_dependency(bar_extra_dep) + repo.add_package(package_foo) + + bar_deps = {"baz": {"version": "^1.0", "extras": ["extra"]}} + add_to_repo(repo, "bar", "1.0.0", bar_deps) + add_to_repo(repo, "bar", "1.0.1", bar_deps) + add_to_repo(repo, "baz", "1.0.0") + add_to_repo(repo, "baz", "1.0.1") + + check_solver_result( + root, + provider, + result={"foo": "1.0.0", "bar": "1.0.0", "baz": "1.0.0"}, + locked={ + "foo": get_package("foo", "1.0.0"), + "bar": get_package("bar", "1.0.0"), + "baz": get_package("baz", "1.0.0"), + }, + )