Skip to content

Commit ca70ad6

Browse files
committed
installer: simplify logic for calculating operations regarding extras and fix small bug (python-poetry#9345)
* Operations are completely calculated in `Transaction.calculate_operations()` so that we do not have to remember extra uninstalls between two calculations. * Previously, extras that were not requested were not uninstalled when running `poetry install` (without extras) if there was no lockfile, now it does not matter if there was a lockfile or not. * In `installer._get_extra_packages()` we do not have to distinguish between locked extras and extras in the pyproject.toml because both must be consistent.
1 parent bea241f commit ca70ad6

File tree

3 files changed

+46
-91
lines changed

3 files changed

+46
-91
lines changed

src/poetry/installation/installer.py

Lines changed: 21 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from packaging.utils import canonicalize_name
88

99
from poetry.installation.executor import Executor
10-
from poetry.installation.operations import Install
1110
from poetry.installation.operations import Uninstall
1211
from poetry.installation.operations import Update
1312
from poetry.repositories import Repository
@@ -239,15 +238,17 @@ def _do_install(self) -> int:
239238
source_root=self._env.path.joinpath("src")
240239
):
241240
ops = solver.solve(use_latest=self._whitelist).calculate_operations()
241+
242+
lockfile_repo = LockfileRepository()
243+
self._populate_lockfile_repo(lockfile_repo, ops)
244+
242245
else:
243246
self._io.write_line("<info>Installing dependencies from lock file</>")
244247

245-
locked_repository = self._locker.locked_repository()
246-
247248
if not self._locker.is_fresh():
248249
raise ValueError(
249-
"pyproject.toml changed significantly since poetry.lock was last generated. "
250-
"Run `poetry lock [--no-update]` to fix the lock file."
250+
"pyproject.toml changed significantly since poetry.lock was last"
251+
" generated. Run `poetry lock [--no-update]` to fix the lock file."
251252
)
252253

253254
locker_extras = {
@@ -258,30 +259,25 @@ def _do_install(self) -> int:
258259
if extra not in locker_extras:
259260
raise ValueError(f"Extra [{extra}] is not specified.")
260261

261-
# If we are installing from lock
262-
# Filter the operations by comparing it with what is
263-
# currently installed
264-
ops = self._get_operations_from_lock(locked_repository)
265-
266-
lockfile_repo = LockfileRepository()
267-
uninstalls = self._populate_lockfile_repo(lockfile_repo, ops)
262+
locked_repository = self._locker.locked_repository()
263+
lockfile_repo = locked_repository
268264

269265
if not self.executor.enabled:
270266
# If we are only in lock mode, no need to go any further
271267
self._write_lock_file(lockfile_repo)
272268
return 0
273269

274-
if self._groups is not None:
275-
root = self._package.with_dependency_groups(list(self._groups), only=True)
276-
else:
277-
root = self._package.without_optional_dependency_groups()
278-
279270
if self._io.is_verbose():
280271
self._io.write_line("")
281272
self._io.write_line(
282273
"<info>Finding the necessary packages for the current system</>"
283274
)
284275

276+
if self._groups is not None:
277+
root = self._package.with_dependency_groups(list(self._groups), only=True)
278+
else:
279+
root = self._package.without_optional_dependency_groups()
280+
285281
# We resolve again by only using the lock file
286282
packages = lockfile_repo.packages + locked_repository.packages
287283
pool = RepositoryPool.from_packages(packages, self._config)
@@ -299,31 +295,12 @@ def _do_install(self) -> int:
299295

300296
with solver.use_environment(self._env):
301297
ops = solver.solve(use_latest=self._whitelist).calculate_operations(
302-
with_uninstalls=self._requires_synchronization,
298+
with_uninstalls=self._requires_synchronization or self._update,
303299
synchronize=self._requires_synchronization,
304300
skip_directory=self._skip_directory,
301+
extras=set(self._extras),
305302
)
306303

307-
if not self._requires_synchronization:
308-
# If no packages synchronisation has been requested we need
309-
# to calculate the uninstall operations
310-
from poetry.puzzle.transaction import Transaction
311-
312-
transaction = Transaction(
313-
locked_repository.packages,
314-
[(package, 0) for package in lockfile_repo.packages],
315-
installed_packages=self._installed_repository.packages,
316-
root_package=root,
317-
)
318-
319-
ops = [
320-
op
321-
for op in transaction.calculate_operations(with_uninstalls=True)
322-
if op.job_type == "uninstall"
323-
] + ops
324-
else:
325-
ops = uninstalls + ops
326-
327304
# We need to filter operations so that packages
328305
# not compatible with the current system,
329306
# or optional and not requested, are dropped
@@ -358,50 +335,15 @@ def _execute(self, operations: list[Operation]) -> int:
358335

359336
def _populate_lockfile_repo(
360337
self, repo: LockfileRepository, ops: Iterable[Operation]
361-
) -> list[Uninstall]:
362-
uninstalls = []
338+
) -> None:
363339
for op in ops:
364340
if isinstance(op, Uninstall):
365-
uninstalls.append(op)
366341
continue
367342

368343
package = op.target_package if isinstance(op, Update) else op.package
369344
if not repo.has_package(package):
370345
repo.add_package(package)
371346

372-
return uninstalls
373-
374-
def _get_operations_from_lock(
375-
self, locked_repository: Repository
376-
) -> list[Operation]:
377-
installed_repo = self._installed_repository
378-
ops: list[Operation] = []
379-
380-
extra_packages = self._get_extra_packages(locked_repository)
381-
for locked in locked_repository.packages:
382-
is_installed = False
383-
for installed in installed_repo.packages:
384-
if locked.name == installed.name:
385-
is_installed = True
386-
if locked.optional and locked.name not in extra_packages:
387-
# Installed but optional and not requested in extras
388-
ops.append(Uninstall(locked))
389-
elif locked.version != installed.version:
390-
ops.append(Update(installed, locked))
391-
392-
# If it's optional and not in required extras
393-
# we do not install
394-
if locked.optional and locked.name not in extra_packages:
395-
continue
396-
397-
op = Install(locked)
398-
if is_installed:
399-
op.skip("Already installed")
400-
401-
ops.append(op)
402-
403-
return ops
404-
405347
def _filter_operations(self, ops: Iterable[Operation], repo: Repository) -> None:
406348
extra_packages = self._get_extra_packages(repo)
407349
for op in ops:
@@ -425,19 +367,11 @@ def _get_extra_packages(self, repo: Repository) -> set[NormalizedName]:
425367
426368
Maybe we just let the solver handle it?
427369
"""
428-
extras: dict[NormalizedName, list[NormalizedName]]
429-
if self._update:
430-
extras = {k: [d.name for d in v] for k, v in self._package.extras.items()}
431-
else:
432-
raw_extras = self._locker.lock_data.get("extras", {})
433-
extras = {
434-
canonicalize_name(extra): [
435-
canonicalize_name(dependency) for dependency in dependencies
436-
]
437-
for extra, dependencies in raw_extras.items()
438-
}
439-
440-
return get_extra_package_names(repo.packages, extras, self._extras)
370+
return get_extra_package_names(
371+
repo.packages,
372+
{k: [d.name for d in v] for k, v in self._package.extras.items()},
373+
self._extras,
374+
)
441375

442376
def _get_installed(self) -> InstalledRepository:
443377
return InstalledRepository.load(self._env)

src/poetry/puzzle/transaction.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
from typing import TYPE_CHECKING
44

5+
from poetry.utils.extras import get_extra_package_names
6+
57

68
if TYPE_CHECKING:
9+
from packaging.utils import NormalizedName
710
from poetry.core.packages.package import Package
811

912
from poetry.installation.operations.operation import Operation
@@ -32,23 +35,42 @@ def calculate_operations(
3235
synchronize: bool = False,
3336
*,
3437
skip_directory: bool = False,
38+
extras: set[NormalizedName] | None = None,
3539
) -> list[Operation]:
3640
from poetry.installation.operations import Install
3741
from poetry.installation.operations import Uninstall
3842
from poetry.installation.operations import Update
3943

4044
operations: list[Operation] = []
4145

46+
extra_packages: set[NormalizedName] = set()
47+
if extras is not None:
48+
assert self._root_package is not None
49+
extra_packages = get_extra_package_names(
50+
[package for package, _ in self._result_packages],
51+
{k: [d.name for d in v] for k, v in self._root_package.extras.items()},
52+
extras,
53+
)
54+
55+
uninstalls: set[NormalizedName] = set()
4256
for result_package, priority in self._result_packages:
4357
installed = False
4458

4559
for installed_package in self._installed_packages:
4660
if result_package.name == installed_package.name:
4761
installed = True
4862

63+
# Extras that were not requested are always uninstalled.
64+
if extras is not None and (
65+
result_package.optional
66+
and result_package.name not in extra_packages
67+
):
68+
uninstalls.add(installed_package.name)
69+
operations.append(Uninstall(installed_package))
70+
4971
# We have to perform an update if the version or another
5072
# attribute of the package has changed (source type, url, ref, ...).
51-
if result_package.version != installed_package.version or (
73+
elif result_package.version != installed_package.version or (
5274
(
5375
# This has to be done because installed packages cannot
5476
# have type "legacy". If a package with type "legacy"
@@ -81,7 +103,6 @@ def calculate_operations(
81103
operations.append(Install(result_package, priority=priority))
82104

83105
if with_uninstalls:
84-
uninstalls: set[str] = set()
85106
for current_package in self._current_packages:
86107
found = any(
87108
current_package.name == result_package.name
@@ -92,7 +113,7 @@ def calculate_operations(
92113
for installed_package in self._installed_packages:
93114
if installed_package.name == current_package.name:
94115
uninstalls.add(installed_package.name)
95-
operations.append(Uninstall(current_package))
116+
operations.append(Uninstall(installed_package))
96117

97118
if synchronize:
98119
result_package_names = {

tests/installation/test_installer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@ def test_run_installs_extras_with_deps_if_requested(
10891089
expected_installations_count = 0 if is_installed else 2
10901090
# We only want to uninstall extras if we do a "poetry install" without extras,
10911091
# not if we do a "poetry update" or "poetry add".
1092-
expected_removals_count = 2 if is_installed and is_locked else 0
1092+
expected_removals_count = 2 if is_installed else 0
10931093

10941094
assert installer.executor.installations_count == expected_installations_count
10951095
assert installer.executor.removals_count == expected_removals_count

0 commit comments

Comments
 (0)