Skip to content

Commit cdfd955

Browse files
perf: let Transaction.calculate_operations run in O(N) (#8063)
Co-authored-by: Randy Döring <[email protected]>
1 parent b8a5fb6 commit cdfd955

File tree

3 files changed

+55
-58
lines changed

3 files changed

+55
-58
lines changed

src/poetry/puzzle/transaction.py

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(
3131
if installed_packages is None:
3232
installed_packages = []
3333

34-
self._installed_packages = installed_packages
34+
self._installed_packages = {pkg.name: pkg for pkg in installed_packages}
3535
self._root_package = root_package
3636
self._marker_env = marker_env
3737
self._groups = groups
@@ -102,51 +102,40 @@ def calculate_operations(
102102
if not is_unsolicited_extra:
103103
relevant_result_packages.add(result_package.name)
104104

105-
installed = False
106-
for installed_package in self._installed_packages:
107-
if result_package.name == installed_package.name:
108-
installed = True
109-
110-
# Extras that were not requested are always uninstalled.
111-
if is_unsolicited_extra:
112-
pending_extra_uninstalls.append(installed_package)
113-
114-
# We have to perform an update if the version or another
115-
# attribute of the package has changed (source type, url, ref, ...).
116-
elif result_package.version != installed_package.version or (
117-
(
118-
# This has to be done because installed packages cannot
119-
# have type "legacy". If a package with type "legacy"
120-
# is installed, the installed package has no source_type.
121-
# Thus, if installed_package has no source_type and
122-
# the result_package has source_type "legacy" (negation of
123-
# the following condition), update must not be performed.
124-
# This quirk has the side effect that when switching
125-
# from PyPI to legacy (or vice versa),
126-
# no update is performed.
127-
installed_package.source_type
128-
or result_package.source_type != "legacy"
129-
)
130-
and not result_package.is_same_package_as(installed_package)
131-
):
132-
operations.append(
133-
Update(
134-
installed_package,
135-
result_package,
136-
priority=priorities[result_package],
137-
)
138-
)
139-
else:
140-
operations.append(
141-
Install(result_package).skip("Already installed")
105+
if installed_package := self._installed_packages.get(result_package.name):
106+
# Extras that were not requested are always uninstalled.
107+
if is_unsolicited_extra:
108+
pending_extra_uninstalls.append(installed_package)
109+
110+
# We have to perform an update if the version or another
111+
# attribute of the package has changed (source type, url, ref, ...).
112+
elif result_package.version != installed_package.version or (
113+
(
114+
# This has to be done because installed packages cannot
115+
# have type "legacy". If a package with type "legacy"
116+
# is installed, the installed package has no source_type.
117+
# Thus, if installed_package has no source_type and
118+
# the result_package has source_type "legacy" (negation of
119+
# the following condition), update must not be performed.
120+
# This quirk has the side effect that when switching
121+
# from PyPI to legacy (or vice versa),
122+
# no update is performed.
123+
installed_package.source_type
124+
or result_package.source_type != "legacy"
125+
)
126+
and not result_package.is_same_package_as(installed_package)
127+
):
128+
operations.append(
129+
Update(
130+
installed_package,
131+
result_package,
132+
priority=priorities[result_package],
142133
)
134+
)
135+
else:
136+
operations.append(Install(result_package).skip("Already installed"))
143137

144-
break
145-
146-
if not (
147-
installed
148-
or (skip_directory and result_package.source_type == "directory")
149-
):
138+
elif not (skip_directory and result_package.source_type == "directory"):
150139
op = Install(result_package, priority=priorities[result_package])
151140
if is_unsolicited_extra:
152141
op.skip("Not required")
@@ -161,21 +150,23 @@ def calculate_operations(
161150

162151
if with_uninstalls:
163152
for current_package in self._current_packages:
164-
found = current_package.name in (relevant_result_packages | uninstalls)
165-
166-
if not found:
167-
for installed_package in self._installed_packages:
168-
if installed_package.name == current_package.name:
169-
uninstalls.add(installed_package.name)
170-
if installed_package.name not in system_site_packages:
171-
operations.append(Uninstall(installed_package))
153+
if current_package.name not in (
154+
relevant_result_packages | uninstalls
155+
) and (
156+
installed_package := self._installed_packages.get(
157+
current_package.name
158+
)
159+
):
160+
uninstalls.add(installed_package.name)
161+
if installed_package.name not in system_site_packages:
162+
operations.append(Uninstall(installed_package))
172163

173164
if synchronize:
174165
# We preserve pip when not managed by poetry, this is done to avoid
175166
# externally managed virtual environments causing unnecessary removals.
176167
preserved_package_names = {"pip"} - relevant_result_packages
177168

178-
for installed_package in self._installed_packages:
169+
for installed_package in self._installed_packages.values():
179170
if installed_package.name in uninstalls:
180171
continue
181172

tests/console/commands/self/conftest.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@
2828
from tests.helpers import TestRepository
2929

3030

31+
@pytest.fixture
32+
def poetry_package() -> Package:
33+
return Package("poetry", __version__)
34+
35+
3136
@pytest.fixture(autouse=True)
32-
def _patch_repos(repo: TestRepository, installed: Repository) -> None:
33-
poetry = Package("poetry", __version__)
34-
repo.add_package(poetry)
35-
installed.add_package(poetry)
37+
def _patch_repos(
38+
repo: TestRepository, installed: Repository, poetry_package: Package
39+
) -> None:
40+
repo.add_package(poetry_package)
41+
installed.add_package(poetry_package)
3642

3743

3844
@pytest.fixture()

tests/console/commands/self/test_add_plugins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ def test_adding_a_plugin_can_update_poetry_dependencies_if_needed(
280280
tester: CommandTester,
281281
repo: TestRepository,
282282
installed: TestRepository,
283+
poetry_package: Package,
283284
) -> None:
284-
poetry_package = Package("poetry", "1.2.0")
285285
poetry_package.add_dependency(Factory.create_dependency("tomlkit", "^0.7.0"))
286286

287287
plugin_package = Package("poetry-plugin", "1.2.3")

0 commit comments

Comments
 (0)