diff --git a/docs/cli.md b/docs/cli.md index ecffe556a0f..22ad4049837 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -204,12 +204,14 @@ poetry install -E mysql -E pgsql poetry install --all-extras ``` -Any extras not specified will always be removed. +Any extras not specified will be kept but not installed: ```bash -poetry install --extras "A B" # C is removed +poetry install --extras "A B" # C is kept if already installed ``` +If you want to remove unspecified extras, use the `sync` command. + By default `poetry` will install your project's package every time you run `install`: ```bash diff --git a/src/poetry/puzzle/transaction.py b/src/poetry/puzzle/transaction.py index df94dc6ae48..3c76bda3876 100644 --- a/src/poetry/puzzle/transaction.py +++ b/src/poetry/puzzle/transaction.py @@ -78,7 +78,6 @@ def calculate_operations( else: priorities = defaultdict(int) relevant_result_packages: set[NormalizedName] = set() - pending_extra_uninstalls: list[Package] = [] # list for deterministic order for result_package in self._result_packages: is_unsolicited_extra = False if self._marker_env: @@ -103,9 +102,9 @@ def calculate_operations( relevant_result_packages.add(result_package.name) if installed_package := self._installed_packages.get(result_package.name): - # Extras that were not requested are always uninstalled. + # Extras that were not requested are not relevant. if is_unsolicited_extra: - pending_extra_uninstalls.append(installed_package) + pass # We have to perform an update if the version or another # attribute of the package has changed (source type, url, ref, ...). @@ -141,14 +140,9 @@ def calculate_operations( op.skip("Not required") operations.append(op) - uninstalls: set[NormalizedName] = set() - for package in pending_extra_uninstalls: - if package.name not in (relevant_result_packages | uninstalls): - uninstalls.add(package.name) - if package.name not in system_site_packages: - operations.append(Uninstall(package)) - if with_uninstalls: + uninstalls: set[NormalizedName] = set() + result_packages = {package.name for package in self._result_packages} for current_package in self._current_packages: if current_package.name not in (result_packages | uninstalls) and ( diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index daeb9e52796..e449436341e 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -1400,6 +1400,7 @@ def test_run_with_different_dependency_extras( @pytest.mark.parametrize("is_locked", [False, True]) @pytest.mark.parametrize("is_installed", [False, True]) @pytest.mark.parametrize("with_extras", [False, True]) +@pytest.mark.parametrize("do_update", [False, True]) @pytest.mark.parametrize("do_sync", [False, True]) def test_run_installs_extras_with_deps_if_requested( installer: Installer, @@ -1410,6 +1411,7 @@ def test_run_installs_extras_with_deps_if_requested( is_locked: bool, is_installed: bool, with_extras: bool, + do_update: bool, do_sync: bool, ) -> None: package.extras = {canonicalize_name("foo"): [get_dependency("C")]} @@ -1443,6 +1445,7 @@ def test_run_installs_extras_with_deps_if_requested( if with_extras: installer.extras(["foo"]) + installer.update(do_update) installer.requires_synchronization(do_sync) result = installer.run() assert result == 0 @@ -1459,7 +1462,7 @@ def test_run_installs_extras_with_deps_if_requested( expected_installations_count = 0 if is_installed else 2 # We only want to uninstall extras if we do a "poetry install" without extras, # not if we do a "poetry update" or "poetry add". - expected_removals_count = 2 if is_installed else 0 + expected_removals_count = 2 if is_installed and do_sync else 0 assert installer.executor.installations_count == expected_installations_count assert installer.executor.removals_count == expected_removals_count diff --git a/tests/puzzle/test_transaction.py b/tests/puzzle/test_transaction.py index a368adf0d31..1a31deb5369 100644 --- a/tests/puzzle/test_transaction.py +++ b/tests/puzzle/test_transaction.py @@ -433,8 +433,10 @@ def test_calculate_operations_extras( if extras: ops = [{"job": "install", "package": Package("a", "1"), "skipped": installed}] elif installed: - # extras are always removed, even if with_uninstalls is False - ops = [{"job": "remove", "package": Package("a", "1")}] + if with_uninstalls and sync: + ops = [{"job": "remove", "package": Package("a", "1")}] + else: + ops = [] else: ops = [{"job": "install", "package": Package("a", "1"), "skipped": True}] @@ -494,6 +496,7 @@ def test_calculate_operations_extras_no_redundant_uninstall(extra: str) -> None: check_operations( transaction.calculate_operations( + synchronize=True, extras=set() if not extra else {canonicalize_name(extra)}, ), ops,