Commit 7049bd5d by Petter Strandmark Committed by GitHub

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

* Add a --keep-untracked option to the install command.

* Call the option --remove-untracked instead.

* Add test and fix Installer.

* Add documentation.

* Make sure to never remove essential packages like pip, setuptools and the root.

* Move logic to the Solver instead.

* Add a few unit tests for Solver.

* Add type hints to solver.py.

Co-authored-by: Steph Samson <hello@stephsamson.com>

* Run black after commit fron Github.

* Import the identifiers used in type annotations.

Co-authored-by: Steph Samson <hello@stephsamson.com>
parent be1b4881
...@@ -116,6 +116,13 @@ the `--no-dev` option. ...@@ -116,6 +116,13 @@ the `--no-dev` option.
poetry install --no-dev 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 You can also specify the extras you want installed
by passing the `--E|--extras` option (See [Extras](#extras) for more info) by passing the `--E|--extras` option (See [Extras](#extras) for more info)
......
...@@ -20,6 +20,9 @@ class InstallCommand(EnvCommand): ...@@ -20,6 +20,9 @@ class InstallCommand(EnvCommand):
"(implicitly enables --verbose).", "(implicitly enables --verbose).",
), ),
option( option(
"remove-untracked", None, "Removes packages not present in the lock file.",
),
option(
"extras", "extras",
"E", "E",
"Extra sets of dependencies to install.", "Extra sets of dependencies to install.",
...@@ -57,6 +60,7 @@ exist it will look for <comment>pyproject.toml</> and do the same. ...@@ -57,6 +60,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
installer.extras(extras) installer.extras(extras)
installer.dev_mode(not self.option("no-dev")) installer.dev_mode(not self.option("no-dev"))
installer.dry_run(self.option("dry-run")) installer.dry_run(self.option("dry-run"))
installer.remove_untracked(self.option("remove-untracked"))
installer.verbose(self.option("verbose")) installer.verbose(self.option("verbose"))
return_code = installer.run() return_code = installer.run()
......
...@@ -38,6 +38,7 @@ class Installer: ...@@ -38,6 +38,7 @@ class Installer:
self._pool = pool self._pool = pool
self._dry_run = False self._dry_run = False
self._remove_untracked = False
self._update = False self._update = False
self._verbose = False self._verbose = False
self._write_lock = True self._write_lock = True
...@@ -82,6 +83,14 @@ class Installer: ...@@ -82,6 +83,14 @@ class Installer:
def is_dry_run(self): # type: () -> bool def is_dry_run(self): # type: () -> bool
return self._dry_run 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 def verbose(self, verbose=True): # type: (bool) -> Installer
self._verbose = verbose self._verbose = verbose
...@@ -155,6 +164,7 @@ class Installer: ...@@ -155,6 +164,7 @@ class Installer:
self._installed_repository, self._installed_repository,
locked_repository, locked_repository,
self._io, self._io,
remove_untracked=self._remove_untracked,
) )
ops = solver.solve(use_latest=self._whitelist) ops = solver.solve(use_latest=self._whitelist)
...@@ -221,7 +231,12 @@ class Installer: ...@@ -221,7 +231,12 @@ class Installer:
whitelist.append(pkg.name) whitelist.append(pkg.name)
solver = Solver( 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): with solver.use_environment(self._env):
......
...@@ -5,10 +5,15 @@ from typing import Any ...@@ -5,10 +5,15 @@ from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from clikit.io import ConsoleIO
from poetry.core.packages import Package from poetry.core.packages import Package
from poetry.core.packages.project_package import ProjectPackage
from poetry.mixology import resolve_version from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage from poetry.packages import DependencyPackage
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.utils.env import Env from poetry.utils.env import Env
from .exceptions import OverrideNeeded from .exceptions import OverrideNeeded
...@@ -21,7 +26,15 @@ from .provider import Provider ...@@ -21,7 +26,15 @@ from .provider import Provider
class Solver: 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._package = package
self._pool = pool self._pool = pool
self._installed = installed self._installed = installed
...@@ -29,6 +42,7 @@ class Solver: ...@@ -29,6 +42,7 @@ class Solver:
self._io = io self._io = io
self._provider = Provider(self._package, self._pool, self._io) self._provider = Provider(self._package, self._pool, self._io)
self._overrides = [] self._overrides = []
self._remove_untracked = remove_untracked
@property @property
def provider(self): # type: () -> Provider def provider(self): # type: () -> Provider
...@@ -132,6 +146,18 @@ class Solver: ...@@ -132,6 +146,18 @@ class Solver:
operations.append(op) 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( return sorted(
operations, operations,
key=lambda o: ( key=lambda o: (
......
...@@ -296,6 +296,59 @@ def test_run_install_no_dev(installer, locker, repo, package, installed): ...@@ -296,6 +296,59 @@ def test_run_install_no_dev(installer, locker, repo, package, installed):
assert len(removals) == 1 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): def test_run_whitelist_add(installer, locker, repo, package):
locker.locked(True) locker.locked(True)
locker.mock_lock_data( locker.mock_lock_data(
......
...@@ -1851,3 +1851,25 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies( ...@@ -1851,3 +1851,25 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies(
{"job": "install", "package": package_a}, {"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, [])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment