Skip to content

Commit 0ac2c1a

Browse files
radoeringabn
authored andcommitted
fix: update, add and remove shall not uninstall extra dependencies
With this change unrequested extras dependencies will also be kept when running `install` and are only removed when running `sync`!
1 parent 32ec5cb commit 0ac2c1a

File tree

4 files changed

+17
-15
lines changed

4 files changed

+17
-15
lines changed

docs/cli.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,14 @@ poetry install -E mysql -E pgsql
204204
poetry install --all-extras
205205
```
206206

207-
Any extras not specified will always be removed.
207+
Any extras not specified will be kept but not installed:
208208

209209
```bash
210-
poetry install --extras "A B" # C is removed
210+
poetry install --extras "A B" # C is kept if already installed
211211
```
212212

213+
If you want to remove unspecified extras, use the `sync` command.
214+
213215
By default `poetry` will install your project's package every time you run `install`:
214216

215217
```bash

src/poetry/puzzle/transaction.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ def calculate_operations(
7878
else:
7979
priorities = defaultdict(int)
8080
relevant_result_packages: set[NormalizedName] = set()
81-
pending_extra_uninstalls: list[Package] = [] # list for deterministic order
8281
for result_package in self._result_packages:
8382
is_unsolicited_extra = False
8483
if self._marker_env:
@@ -103,9 +102,9 @@ def calculate_operations(
103102
relevant_result_packages.add(result_package.name)
104103

105104
if installed_package := self._installed_packages.get(result_package.name):
106-
# Extras that were not requested are always uninstalled.
105+
# Extras that were not requested are not relevant.
107106
if is_unsolicited_extra:
108-
pending_extra_uninstalls.append(installed_package)
107+
pass
109108

110109
# We have to perform an update if the version or another
111110
# attribute of the package has changed (source type, url, ref, ...).
@@ -141,14 +140,9 @@ def calculate_operations(
141140
op.skip("Not required")
142141
operations.append(op)
143142

144-
uninstalls: set[NormalizedName] = set()
145-
for package in pending_extra_uninstalls:
146-
if package.name not in (relevant_result_packages | uninstalls):
147-
uninstalls.add(package.name)
148-
if package.name not in system_site_packages:
149-
operations.append(Uninstall(package))
150-
151143
if with_uninstalls:
144+
uninstalls: set[NormalizedName] = set()
145+
152146
result_packages = {package.name for package in self._result_packages}
153147
for current_package in self._current_packages:
154148
if current_package.name not in (result_packages | uninstalls) and (

tests/installation/test_installer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,7 @@ def test_run_with_different_dependency_extras(
14001400
@pytest.mark.parametrize("is_locked", [False, True])
14011401
@pytest.mark.parametrize("is_installed", [False, True])
14021402
@pytest.mark.parametrize("with_extras", [False, True])
1403+
@pytest.mark.parametrize("do_update", [False, True])
14031404
@pytest.mark.parametrize("do_sync", [False, True])
14041405
def test_run_installs_extras_with_deps_if_requested(
14051406
installer: Installer,
@@ -1410,6 +1411,7 @@ def test_run_installs_extras_with_deps_if_requested(
14101411
is_locked: bool,
14111412
is_installed: bool,
14121413
with_extras: bool,
1414+
do_update: bool,
14131415
do_sync: bool,
14141416
) -> None:
14151417
package.extras = {canonicalize_name("foo"): [get_dependency("C")]}
@@ -1443,6 +1445,7 @@ def test_run_installs_extras_with_deps_if_requested(
14431445

14441446
if with_extras:
14451447
installer.extras(["foo"])
1448+
installer.update(do_update)
14461449
installer.requires_synchronization(do_sync)
14471450
result = installer.run()
14481451
assert result == 0
@@ -1459,7 +1462,7 @@ def test_run_installs_extras_with_deps_if_requested(
14591462
expected_installations_count = 0 if is_installed else 2
14601463
# We only want to uninstall extras if we do a "poetry install" without extras,
14611464
# not if we do a "poetry update" or "poetry add".
1462-
expected_removals_count = 2 if is_installed else 0
1465+
expected_removals_count = 2 if is_installed and do_sync else 0
14631466

14641467
assert installer.executor.installations_count == expected_installations_count
14651468
assert installer.executor.removals_count == expected_removals_count

tests/puzzle/test_transaction.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,10 @@ def test_calculate_operations_extras(
433433
if extras:
434434
ops = [{"job": "install", "package": Package("a", "1"), "skipped": installed}]
435435
elif installed:
436-
# extras are always removed, even if with_uninstalls is False
437-
ops = [{"job": "remove", "package": Package("a", "1")}]
436+
if with_uninstalls and sync:
437+
ops = [{"job": "remove", "package": Package("a", "1")}]
438+
else:
439+
ops = []
438440
else:
439441
ops = [{"job": "install", "package": Package("a", "1"), "skipped": True}]
440442

@@ -494,6 +496,7 @@ def test_calculate_operations_extras_no_redundant_uninstall(extra: str) -> None:
494496

495497
check_operations(
496498
transaction.calculate_operations(
499+
synchronize=True,
497500
extras=set() if not extra else {canonicalize_name(extra)},
498501
),
499502
ops,

0 commit comments

Comments
 (0)