Skip to content

Commit

Permalink
Fix python-poetry#2658: Run all uninstalls serially
Browse files Browse the repository at this point in the history
  • Loading branch information
PetterS committed Oct 6, 2020
1 parent b02e66c commit 6be0ae7
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 16 deletions.
51 changes: 35 additions & 16 deletions poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,48 @@ def execute(self, operations): # type: (Operation) -> int
self._display_summary(operations)

# We group operations by priority
groups = itertools.groupby(operations, key=lambda o: -o.priority)
unique = 0

def groupkey(op):
if op.job_type == "uninstall":
# See Github issue https://github.com/python-poetry/poetry/issues/2658
# Running uninstalls in parallel is not safe. Therefore, we run each
# uninstall in its separate group.
nonlocal unique
unique += 1
return unique
else:
# We group the rest of the operations by priority.
return -op.priority

groups = itertools.groupby(operations, key=groupkey)
self._sections = OrderedDict()
for _, group in groups:
tasks = []
for operation in group:
if self._shutdown:
break

tasks.append(self._executor.submit(self._execute_operation, operation))
if self._execute_group(tuple(group)) == 1:
return 1

try:
wait(tasks)
except KeyboardInterrupt:
self._shutdown = True
return 0

def _execute_group(self, group): # type: (Operation) -> int
tasks = []
for operation in group:
if self._shutdown:
# Cancelling further tasks from being executed
[task.cancel() for task in tasks]
self._executor.shutdown(wait=True)

break

return 1 if self._shutdown else 0
tasks.append(self._executor.submit(self._execute_operation, operation))

try:
wait(tasks)
except KeyboardInterrupt:
self._shutdown = True

if self._shutdown:
# Cancelling further tasks from being executed
[task.cancel() for task in tasks]
self._executor.shutdown(wait=True)
return 1

return 0

def _write(self, operation, line):
if not self.supports_fancy_output() or not self._should_write_operation(
Expand Down
25 changes: 25 additions & 0 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import re
import shutil

from unittest import mock

import pytest

from clikit.api.formatter.style import Style
Expand Down Expand Up @@ -126,6 +128,29 @@ def test_execute_executes_a_batch_of_operations(
assert 5 == len(env.executed)


def test_execute_uninstalls_serially(config, io):
executor = Executor(None, None, Config(), io)
executor._execute_group = mock.Mock()

assert 0 == executor.execute(
[
Uninstall(Package("pytest", "3.5.2")),
Uninstall(Package("attrs", "17.4.0")),
Uninstall(Package("requests", "2.18.3")),
Uninstall(Package("clikit", "0.2.3")),
]
)

executed_groups = []
for call in executor._execute_group.mock_calls:
ops = call.args[0]
assert all(op.job_type == "uninstall" for op in ops)
executed_groups.append({op.package.name for op in ops})

# Uninstalls executed serially in separate groups.
assert executed_groups == [{"pytest"}, {"attrs"}, {"requests"}, {"clikit"}]


def test_execute_shows_skipped_operations_if_verbose(config, pool, io):
config = Config()
config.merge({"cache-dir": "/foo"})
Expand Down

0 comments on commit 6be0ae7

Please sign in to comment.