Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,21 @@ values, usage instructions and warnings.

Use parallel execution when using the new (`>=1.1.0`) installer.

### `installer.builtin-uninstall`

**Type**: `boolean`

**Default**: `false`

**Environment Variable**: `POETRY_INSTALLER_BUILTIN_UNINSTALL`

*Introduced in 2.5.0*

If set to `true`, Poetry uninstalls packages using its own built-in routine instead of
invoking `pip uninstall` as a subprocess. This avoids the overhead of spawning pip.
Behavior is otherwise equivalent: confirmation is automatic, and files outside the
target environment's prefix are never removed.

### `installer.build-config-settings.<package-name>`

**Type**: `Serialised JSON with string or list of string properties`
Expand Down
2 changes: 2 additions & 0 deletions src/poetry/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class Config:
"no-binary": None,
"only-binary": None,
"build-config-settings": {},
"builtin-uninstall": False,
},
"python": {"installation-dir": os.path.join("{data-dir}", "python")},
"solver": {
Expand Down Expand Up @@ -396,6 +397,7 @@ def _get_normalizer(name: str | Sequence[str]) -> Callable[[str], Any]:
"virtualenvs.use-poetry-python",
"installer.re-resolve",
"installer.parallel",
"installer.builtin-uninstall",
"solver.lazy-wheel",
"system-git-client",
"keyring.enabled",
Expand Down
1 change: 1 addition & 0 deletions src/poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]:
"requests.max-retries": (lambda val: int(val) >= 0, int_normalizer),
"installer.re-resolve": (boolean_validator, boolean_normalizer),
"installer.parallel": (boolean_validator, boolean_normalizer),
"installer.builtin-uninstall": (boolean_validator, boolean_normalizer),
"installer.max-workers": (lambda val: int(val) > 0, int_normalizer),
"installer.no-binary": (
PackageFilterPolicy.validator,
Expand Down
21 changes: 17 additions & 4 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from poetry.installation.operations import Install
from poetry.installation.operations import Uninstall
from poetry.installation.operations import Update
from poetry.installation.uninstaller import uninstall_distribution
from poetry.installation.wheel_installer import WheelInstaller
from poetry.puzzle.exceptions import SolverProblemError
from poetry.utils._compat import decode
Expand Down Expand Up @@ -78,6 +79,9 @@ def __init__(
self._verbose = False
self._wheel_installer = WheelInstaller(self._env)
self._build_constraints = build_constraints or {}
self._use_builtin_uninstall: bool = config.get(
"installer.builtin-uninstall", False
)

if parallel is None:
parallel = config.get("installer.parallel", True)
Expand Down Expand Up @@ -605,11 +609,12 @@ def _install(self, operation: Install | Update) -> int:

try:
if operation.job_type == "update":
# Uninstall first
# TODO: Make an uninstaller and find a way to rollback in case
# the new package can't be installed
# Uninstall the old version first. If this fails, the install is
# aborted so the new version is not installed over the old one.
assert isinstance(operation, Update)
self._remove(operation.initial_package)
result_code = self._remove(operation.initial_package)
if result_code != 0:
return result_code

self._wheel_installer.install(archive)
finally:
Expand All @@ -628,6 +633,14 @@ def _remove(self, package: Package) -> int:
if src_dir.exists():
remove_directory(src_dir, force=True)

if self._use_builtin_uninstall:
# The builtin uninstaller removes the files directly and raises if a
# file cannot be removed. uninstall_distribution returns None when
# there is nothing to uninstall (not installed, outside env, in
# stdlib, missing RECORD); treat that as a successful no-op.
uninstall_distribution(self._env, package.name)
return 0

try:
return self.run_pip("uninstall", package.name, "-y")
except EnvCommandError as e:
Expand Down
Loading
Loading