Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a --remove-untracked option to the install command. #2172

Merged
merged 13 commits into from
May 1, 2020
7 changes: 7 additions & 0 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ the `--no-dev` option.
poetry install --no-dev
```

If you want to remove old dependencies no longer present in the lock file, use the
`--remove-untracked` option.

```bash
poetry install --remove-untracked
```

You can also specify the extras you want installed
by passing the `--E|--extras` option (See [Extras](#extras) for more info)

Expand Down
4 changes: 4 additions & 0 deletions poetry/console/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class InstallCommand(EnvCommand):
"Output the operations but do not execute anything "
"(implicitly enables --verbose).",
),
option(
"remove-untracked", None, "Removes packages not present in the lock file.",
),
option(
"extras",
"E",
Expand Down Expand Up @@ -57,6 +60,7 @@ def handle(self):
installer.extras(extras)
installer.dev_mode(not self.option("no-dev"))
installer.dry_run(self.option("dry-run"))
installer.remove_untracked(self.option("remove-untracked"))
installer.verbose(self.option("verbose"))

return_code = installer.run()
Expand Down
17 changes: 16 additions & 1 deletion poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
self._pool = pool

self._dry_run = False
self._remove_untracked = False
self._update = False
self._verbose = False
self._write_lock = True
Expand Down Expand Up @@ -82,6 +83,14 @@ def dry_run(self, dry_run=True): # type: (bool) -> Installer
def is_dry_run(self): # type: () -> bool
return self._dry_run

def remove_untracked(self, remove_untracked=True): # type: (bool) -> Installer
self._remove_untracked = remove_untracked

return self

def is_remove_untracked(self): # type: () -> bool
return self._remove_untracked

def verbose(self, verbose=True): # type: (bool) -> Installer
self._verbose = verbose

Expand Down Expand Up @@ -155,6 +164,7 @@ def _do_install(self, local_repo):
self._installed_repository,
locked_repository,
self._io,
remove_untracked=self._remove_untracked,
)

ops = solver.solve(use_latest=self._whitelist)
Expand Down Expand Up @@ -221,7 +231,12 @@ def _do_install(self, local_repo):
whitelist.append(pkg.name)

solver = Solver(
root, pool, self._installed_repository, locked_repository, NullIO()
root,
pool,
self._installed_repository,
locked_repository,
NullIO(),
remove_untracked=self._remove_untracked,
)

with solver.use_environment(self._env):
Expand Down
28 changes: 27 additions & 1 deletion poetry/puzzle/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
from typing import Dict
from typing import List

from clikit.io import ConsoleIO

from poetry.core.packages import Package
from poetry.core.packages.project_package import ProjectPackage
from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.utils.env import Env

from .exceptions import OverrideNeeded
Expand All @@ -21,14 +26,23 @@


class Solver:
def __init__(self, package, pool, installed, locked, io):
def __init__(
self,
package, # type: ProjectPackage
pool, # type: Pool
installed, # type: Repository
locked, # type: Repository
io, # type: ConsoleIO
remove_untracked=False, # type: bool
):
self._package = package
self._pool = pool
self._installed = installed
self._locked = locked
self._io = io
self._provider = Provider(self._package, self._pool, self._io)
self._overrides = []
self._remove_untracked = remove_untracked

@property
def provider(self): # type: () -> Provider
Expand Down Expand Up @@ -132,6 +146,18 @@ def solve(self, use_latest=None): # type: (...) -> List[Operation]

operations.append(op)

if self._remove_untracked:
locked_names = {locked.name for locked in self._locked.packages}

for installed in self._installed.packages:
if installed.name == self._package.name:
continue
if installed.name in Provider.UNSAFE_PACKAGES:
# Never remove pip, setuptools etc.
continue
if installed.name not in locked_names:
operations.append(Uninstall(installed))

return sorted(
operations,
key=lambda o: (
Expand Down
53 changes: 53 additions & 0 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,59 @@ def test_run_install_no_dev(installer, locker, repo, package, installed):
assert len(removals) == 1


def test_run_install_remove_untracked(installer, locker, repo, package, installed):
locker.locked(True)
locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "1.0",
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
}
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {"a": []},
},
}
)
package_a = get_package("a", "1.0")
package_b = get_package("b", "1.1")
package_c = get_package("c", "1.2")
package_pip = get_package("pip", "20.0.0")
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_pip)

installed.add_package(package_a)
installed.add_package(package_b)
installed.add_package(package_c)
installed.add_package(package_pip) # Always required and never removed.
installed.add_package(package) # Root package never removed.

package.add_dependency("A", "~1.0")

installer.dev_mode(True).remove_untracked(True)
installer.run()

installs = installer.installer.installs
assert len(installs) == 0

updates = installer.installer.updates
assert len(updates) == 0

removals = installer.installer.removals
assert set(r.name for r in removals) == {"b", "c"}


def test_run_whitelist_add(installer, locker, repo, package):
locker.locked(True)
locker.mock_lock_data(
Expand Down
22 changes: 22 additions & 0 deletions tests/puzzle/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1851,3 +1851,25 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies(
{"job": "install", "package": package_a},
],
)


def test_solver_remove_untracked_single(package, pool, installed, locked, io):
solver = Solver(package, pool, installed, locked, io, remove_untracked=True)
package_a = get_package("a", "1.0")
installed.add_package(package_a)

ops = solver.solve()

check_solver_result(ops, [{"job": "remove", "package": package_a}])


def test_solver_remove_untracked_keeps_critical_package(
package, pool, installed, locked, io
):
solver = Solver(package, pool, installed, locked, io, remove_untracked=True)
package_pip = get_package("pip", "1.0")
installed.add_package(package_pip)

ops = solver.solve()

check_solver_result(ops, [])