Commit 1da3e729 by Sébastien Eustace Committed by GitHub

Merge pull request #4336 from python-poetry/improve-operations-transactions

Improve package operations management
parents df77c800 ea8fb8c6
...@@ -163,11 +163,19 @@ The `--dev-only` option is now deprecated. You should use the `--only dev` notat ...@@ -163,11 +163,19 @@ The `--dev-only` option is now deprecated. You should use the `--only dev` notat
See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information
about dependency groups. about dependency groups.
If you want to remove old dependencies no longer present in the lock file, use the If you want to synchronize your environment – and ensure it matches the lock file – use the
`--remove-untracked` option. `--sync` option.
```bash ```bash
poetry install --remove-untracked poetry install --sync
```
The `--sync` can be combined with group-related options:
```bash
poetry install --without dev --sync
poetry install --with docs --sync
poetry install --only dev
``` ```
You can also specify the extras you want installed You can also specify the extras you want installed
...@@ -204,12 +212,14 @@ option is used. ...@@ -204,12 +212,14 @@ option is used.
* `--with`: The optional dependency groups to include for installation. * `--with`: The optional dependency groups to include for installation.
* `--only`: The only dependency groups to install. * `--only`: The only dependency groups to install.
* `--default`: Only install the default dependencies. * `--default`: Only install the default dependencies.
* `--no-dev`: Do not install dev dependencies. (**Deprecated**) * `--sync`: Synchronize the environment with the locked packages and the specified groups.
* `--dev-only`: Only install dev dependencies. (**Deprecated**)
* `--no-root`: Do not install the root package (your project). * `--no-root`: Do not install the root package (your project).
* `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). * `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose).
* `--remove-untracked`: Remove dependencies not presented in the lock file
* `--extras (-E)`: Features to install (multiple values allowed). * `--extras (-E)`: Features to install (multiple values allowed).
* `--no-dev`: Do not install dev dependencies. (**Deprecated**)
* `--dev-only`: Only install dev dependencies. (**Deprecated**)
* `--remove-untracked`: Remove dependencies not presented in the lock file. (**Deprecated**)
## update ## update
......
...@@ -146,3 +146,29 @@ to remove packages from a specific group: ...@@ -146,3 +146,29 @@ to remove packages from a specific group:
```bash ```bash
poetry remove mkdocs --group docs poetry remove mkdocs --group docs
``` ```
## Synchronizing dependencies
Poetry supports what's called dependency synchronization. What this does is ensuring
that the locked dependencies in the `poetry.lock` file are the only ones present
in the environment, removing anything that's not necessary.
This is done by using the `--sync` option of the `install` command:
```bash
poetry install --sync
```
The `--sync` option can be combined with any [dependency groups]({{< relref "#dependency-groups" >}}) related options
to synchronize the environment with specific groups.
```bash
poetry install --without dev --sync
poetry install --with docs --sync
poetry install --only dev
```
{{% note %}}
The `--sync` option replaces the `--remove-untracked` option which is now deprecated.
{{% /note %}}
...@@ -86,7 +86,7 @@ class DebugResolveCommand(InitCommand): ...@@ -86,7 +86,7 @@ class DebugResolveCommand(InitCommand):
solver = Solver(package, pool, Repository(), Repository(), self._io) solver = Solver(package, pool, Repository(), Repository(), self._io)
ops = solver.solve() ops = solver.solve().calculate_operations()
self.line("") self.line("")
self.line("Resolution results:") self.line("Resolution results:")
...@@ -123,7 +123,7 @@ class DebugResolveCommand(InitCommand): ...@@ -123,7 +123,7 @@ class DebugResolveCommand(InitCommand):
solver = Solver(package, pool, Repository(), Repository(), NullIO()) solver = Solver(package, pool, Repository(), Repository(), NullIO())
with solver.use_environment(env): with solver.use_environment(env):
ops = solver.solve() ops = solver.solve().calculate_operations()
for op in ops: for op in ops:
if self.option("install") and op.skipped: if self.option("install") and op.skipped:
......
...@@ -42,6 +42,11 @@ class InstallCommand(InstallerCommand): ...@@ -42,6 +42,11 @@ class InstallCommand(InstallerCommand):
"Only install the development dependencies. (<warning>Deprecated</warning>)", "Only install the development dependencies. (<warning>Deprecated</warning>)",
), ),
option( option(
"sync",
None,
"Synchronize the environment with the locked packages and the specified groups.",
),
option(
"no-root", None, "Do not install the root package (the current project)." "no-root", None, "Do not install the root package (the current project)."
), ),
option( option(
...@@ -138,11 +143,20 @@ dependencies and not including the current project, run the command with the ...@@ -138,11 +143,20 @@ dependencies and not including the current project, run the command with the
if self.option("default"): if self.option("default"):
only_groups.append("default") only_groups.append("default")
with_synchronization = self.option("sync")
if self.option("remove-untracked"):
self.line(
"<warning>The `<fg=yellow;options=bold>--remove-untracked</>` option is deprecated,"
"use the `<fg=yellow;options=bold>--sync</>` option instead.</warning>"
)
with_synchronization = True
self._installer.only_groups(only_groups) self._installer.only_groups(only_groups)
self._installer.without_groups(excluded_groups) self._installer.without_groups(excluded_groups)
self._installer.with_groups(included_groups) self._installer.with_groups(included_groups)
self._installer.dry_run(self.option("dry-run")) self._installer.dry_run(self.option("dry-run"))
self._installer.remove_untracked(self.option("remove-untracked")) self._installer.requires_synchronization(with_synchronization)
self._installer.verbose(self._io.is_verbose()) self._installer.verbose(self._io.is_verbose())
return_code = self._installer.run() return_code = self._installer.run()
......
...@@ -161,7 +161,7 @@ lists all packages available.""" ...@@ -161,7 +161,7 @@ lists all packages available."""
) )
solver.provider.load_deferred(False) solver.provider.load_deferred(False)
with solver.use_environment(self.env): with solver.use_environment(self.env):
ops = solver.solve() ops = solver.solve().calculate_operations()
required_locked_packages = set([op.package for op in ops if not op.skipped]) required_locked_packages = set([op.package for op in ops if not op.skipped])
......
...@@ -50,7 +50,7 @@ class Installer: ...@@ -50,7 +50,7 @@ class Installer:
self._pool = pool self._pool = pool
self._dry_run = False self._dry_run = False
self._remove_untracked = False self._requires_synchronization = False
self._update = False self._update = False
self._verbose = False self._verbose = False
self._write_lock = True self._write_lock = True
...@@ -122,14 +122,13 @@ class Installer: ...@@ -122,14 +122,13 @@ class Installer:
def is_dry_run(self) -> bool: def is_dry_run(self) -> bool:
return self._dry_run return self._dry_run
def remove_untracked(self, remove_untracked: bool = True) -> "Installer": def requires_synchronization(
self._remove_untracked = remove_untracked self, requires_synchronization: bool = True
) -> "Installer":
self._requires_synchronization = requires_synchronization
return self return self
def is_remove_untracked(self) -> bool:
return self._remove_untracked
def verbose(self, verbose: bool = True) -> "Installer": def verbose(self, verbose: bool = True) -> "Installer":
self._verbose = verbose self._verbose = verbose
self._executor.verbose(verbose) self._executor.verbose(verbose)
...@@ -212,7 +211,7 @@ class Installer: ...@@ -212,7 +211,7 @@ class Installer:
self._io, self._io,
) )
ops = solver.solve(use_latest=[]) ops = solver.solve(use_latest=[]).calculate_operations()
local_repo = Repository() local_repo = Repository()
self._populate_local_repo(local_repo, ops) self._populate_local_repo(local_repo, ops)
...@@ -247,10 +246,9 @@ class Installer: ...@@ -247,10 +246,9 @@ 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).calculate_operations()
else: else:
self._io.write_line("<info>Installing dependencies from lock file</>") self._io.write_line("<info>Installing dependencies from lock file</>")
...@@ -318,19 +316,35 @@ class Installer: ...@@ -318,19 +316,35 @@ class Installer:
pool.add_repository(repo) pool.add_repository(repo)
solver = Solver( solver = Solver(
root, root, pool, self._installed_repository, locked_repository, NullIO()
pool,
self._installed_repository,
locked_repository,
NullIO(),
remove_untracked=self._remove_untracked,
) )
# Everything is resolved at this point, so we no longer need # Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies) # to load deferred dependencies (i.e. VCS, URL and path dependencies)
solver.provider.load_deferred(False) solver.provider.load_deferred(False)
with solver.use_environment(self._env): with solver.use_environment(self._env):
ops = solver.solve(use_latest=self._whitelist) ops = solver.solve(use_latest=self._whitelist).calculate_operations(
with_uninstalls=self._requires_synchronization,
synchronize=self._requires_synchronization,
)
if not self._requires_synchronization:
# If no packages synchronisation has been requested we need
# to calculate the uninstall operations
from poetry.puzzle.transaction import Transaction
transaction = Transaction(
locked_repository.packages,
[(package, 0) for package in local_repo.packages],
installed_packages=self._installed_repository.packages,
root_package=root,
)
ops = [
op
for op in transaction.calculate_operations(with_uninstalls=True)
if op.job_type == "uninstall"
] + ops
# We need to filter operations so that packages # We need to filter operations so that packages
# not compatible with the current system, # not compatible with the current system,
......
...@@ -16,9 +16,6 @@ from cleo.io.io import IO ...@@ -16,9 +16,6 @@ from cleo.io.io import IO
from poetry.core.packages.package import Package from poetry.core.packages.package import Package
from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.project_package import ProjectPackage
from poetry.installation.operations import Install
from poetry.installation.operations import Uninstall
from poetry.installation.operations import Update
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
...@@ -37,7 +34,8 @@ if TYPE_CHECKING: ...@@ -37,7 +34,8 @@ if TYPE_CHECKING:
from poetry.core.packages.file_dependency import FileDependency from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.url_dependency import URLDependency
from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.packages.vcs_dependency import VCSDependency
from poetry.installation.operations import OperationTypes
from .transaction import Transaction
class Solver: class Solver:
...@@ -48,7 +46,6 @@ class Solver: ...@@ -48,7 +46,6 @@ class Solver:
installed: Repository, installed: Repository,
locked: Repository, locked: Repository,
io: IO, io: IO,
remove_untracked: bool = False,
provider: Optional[Provider] = None, provider: Optional[Provider] = None,
): ):
self._package = package self._package = package
...@@ -62,39 +59,19 @@ class Solver: ...@@ -62,39 +59,19 @@ class Solver:
self._provider = provider self._provider = provider
self._overrides = [] self._overrides = []
self._remove_untracked = remove_untracked
self._preserved_package_names = None
@property @property
def provider(self) -> Provider: def provider(self) -> Provider:
return self._provider return self._provider
@property
def preserved_package_names(self):
if self._preserved_package_names is None:
self._preserved_package_names = {
self._package.name,
*Provider.UNSAFE_PACKAGES,
}
deps = {package.name for package in self._locked.packages}
# preserve pip/setuptools/wheel when not managed by poetry, this is so
# to avoid externally managed virtual environments causing unnecessary
# removals.
for name in {"pip", "wheel", "setuptools"}:
if name not in deps:
self._preserved_package_names.add(name)
return self._preserved_package_names
@contextmanager @contextmanager
def use_environment(self, env: Env) -> None: def use_environment(self, env: Env) -> None:
with self.provider.use_environment(env): with self.provider.use_environment(env):
yield yield
def solve(self, use_latest: List[str] = None) -> List["OperationTypes"]: def solve(self, use_latest: List[str] = None) -> "Transaction":
from .transaction import Transaction
with self._provider.progress(): with self._provider.progress():
start = time.time() start = time.time()
packages, depths = self._solve(use_latest=use_latest) packages, depths = self._solve(use_latest=use_latest)
...@@ -110,121 +87,11 @@ class Solver: ...@@ -110,121 +87,11 @@ class Solver:
f"Resolved with overrides: {', '.join(f'({b})' for b in self._overrides)}" f"Resolved with overrides: {', '.join(f'({b})' for b in self._overrides)}"
) )
operations = [] return Transaction(
for i, package in enumerate(packages): self._locked.packages,
installed = False list(zip(packages, depths)),
for pkg in self._installed.packages: installed_packages=self._installed.packages,
if package.name == pkg.name: root_package=self._package,
installed = True
if pkg.source_type == "git" and package.source_type == "git":
from poetry.core.vcs.git import Git
# Trying to find the currently installed version
pkg_source_url = Git.normalize_url(pkg.source_url)
package_source_url = Git.normalize_url(package.source_url)
for locked in self._locked.packages:
if locked.name != pkg.name or locked.source_type != "git":
continue
locked_source_url = Git.normalize_url(locked.source_url)
if (
locked.name == pkg.name
and locked.source_type == pkg.source_type
and locked_source_url == pkg_source_url
and locked.source_reference == pkg.source_reference
and locked.source_resolved_reference
== pkg.source_resolved_reference
):
pkg = Package(
pkg.name,
locked.version,
source_type="git",
source_url=locked.source_url,
source_reference=locked.source_reference,
source_resolved_reference=locked.source_resolved_reference,
)
break
if pkg_source_url != package_source_url or (
(
not pkg.source_resolved_reference
or not package.source_resolved_reference
)
and pkg.source_reference != package.source_reference
and not pkg.source_reference.startswith(
package.source_reference
)
or (
pkg.source_resolved_reference
and package.source_resolved_reference
and pkg.source_resolved_reference
!= package.source_resolved_reference
and not pkg.source_resolved_reference.startswith(
package.source_resolved_reference
)
)
):
operations.append(Update(pkg, package, priority=depths[i]))
else:
operations.append(
Install(package).skip("Already installed")
)
elif package.version != pkg.version:
# Checking version
operations.append(Update(pkg, package, priority=depths[i]))
elif pkg.source_type and package.source_type != pkg.source_type:
operations.append(Update(pkg, package, priority=depths[i]))
else:
operations.append(
Install(package, priority=depths[i]).skip(
"Already installed"
)
)
break
if not installed:
operations.append(Install(package, priority=depths[i]))
# Checking for removals
for pkg in self._locked.packages:
remove = True
for package in packages:
if pkg.name == package.name:
remove = False
break
if remove:
skip = True
for installed in self._installed.packages:
if installed.name == pkg.name:
skip = False
break
op = Uninstall(pkg)
if skip:
op.skip("Not currently installed")
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 in self.preserved_package_names:
continue
if installed.name not in locked_names:
operations.append(Uninstall(installed))
return sorted(
operations,
key=lambda o: (
-o.priority,
o.package.name,
o.package.version,
),
) )
def solve_in_compatibility_mode( def solve_in_compatibility_mode(
......
from typing import TYPE_CHECKING
from typing import List
from typing import Optional
from typing import Tuple
if TYPE_CHECKING:
from poetry.core.packages.package import Package
from poetry.installation.operations import OperationTypes
class Transaction:
def __init__(
self,
current_packages: List["Package"],
result_packages: List[Tuple["Package", int]],
installed_packages: Optional[List["Package"]] = None,
root_package: Optional["Package"] = None,
) -> None:
self._current_packages = current_packages
self._result_packages = result_packages
if installed_packages is None:
installed_packages = []
self._installed_packages = installed_packages
self._root_package = root_package
def calculate_operations(
self, with_uninstalls: bool = True, synchronize: bool = False
) -> List["OperationTypes"]:
from poetry.installation.operations.install import Install
from poetry.installation.operations.uninstall import Uninstall
from poetry.installation.operations.update import Update
operations = []
for result_package, priority in self._result_packages:
installed = False
for installed_package in self._installed_packages:
if result_package.name == installed_package.name:
installed = True
if result_package.version != installed_package.version:
operations.append(
Update(installed_package, result_package, priority=priority)
)
elif (
installed_package.source_type
or result_package.source_type != "legacy"
) and not result_package.is_same_package_as(installed_package):
operations.append(
Update(installed_package, result_package, priority=priority)
)
else:
operations.append(
Install(result_package).skip("Already installed")
)
break
if not installed:
operations.append(Install(result_package, priority=priority))
if with_uninstalls:
for current_package in self._current_packages:
found = False
for result_package, _ in self._result_packages:
if current_package.name == result_package.name:
found = True
break
if not found:
for installed_package in self._installed_packages:
if installed_package.name == current_package.name:
operations.append(Uninstall(current_package))
if synchronize:
current_package_names = {
current_package.name for current_package in self._current_packages
}
# We preserve pip/setuptools/wheel when not managed by poetry, this is done
# to avoid externally managed virtual environments causing unnecessary
# removals.
preserved_package_names = {
"pip",
"setuptools",
"wheel",
} - current_package_names
for installed_package in self._installed_packages:
if (
self._root_package
and installed_package.name == self._root_package.name
):
continue
if installed_package.name in preserved_package_names:
continue
if installed_package.name not in current_package_names:
operations.append(Uninstall(installed_package))
return sorted(
operations,
key=lambda o: (
-o.priority,
o.package.name,
o.package.version,
),
)
...@@ -17,3 +17,14 @@ def test_group_options_are_passed_to_the_installer(tester, mocker): ...@@ -17,3 +17,14 @@ def test_group_options_are_passed_to_the_installer(tester, mocker):
assert tester.command.installer._with_groups == ["foo", "bar"] assert tester.command.installer._with_groups == ["foo", "bar"]
assert tester.command.installer._without_groups == ["baz", "bim"] assert tester.command.installer._without_groups == ["baz", "bim"]
assert tester.command.installer._only_groups == ["bam"] assert tester.command.installer._only_groups == ["bam"]
def test_sync_option_is_passed_to_the_installer(tester, mocker):
"""
The --sync option is passed properly to the installer.
"""
mocker.patch.object(tester.command.installer, "run", return_value=1)
tester.execute("--sync")
assert tester.command.installer._requires_synchronization
...@@ -351,7 +351,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed): ...@@ -351,7 +351,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed):
assert 0 == installer.executor.installations_count assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count assert 0 == installer.executor.updates_count
assert 1 == installer.executor.removals_count assert 0 == installer.executor.removals_count
def test_run_install_group_only(installer, locker, repo, package, installed): def test_run_install_group_only(installer, locker, repo, package, installed):
...@@ -362,7 +362,7 @@ def test_run_install_group_only(installer, locker, repo, package, installed): ...@@ -362,7 +362,7 @@ def test_run_install_group_only(installer, locker, repo, package, installed):
assert 0 == installer.executor.installations_count assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count assert 0 == installer.executor.updates_count
assert 2 == installer.executor.removals_count assert 0 == installer.executor.removals_count
def test_run_install_with_optional_group_not_selected( def test_run_install_with_optional_group_not_selected(
...@@ -376,7 +376,207 @@ def test_run_install_with_optional_group_not_selected( ...@@ -376,7 +376,207 @@ def test_run_install_with_optional_group_not_selected(
assert 0 == installer.executor.installations_count assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count assert 0 == installer.executor.updates_count
assert 1 == installer.executor.removals_count assert 0 == installer.executor.removals_count
def test_run_install_does_not_remove_locked_packages_if_installed_but_not_required(
installer, locker, repo, package, installed
):
package_a = get_package("a", "1.0")
package_b = get_package("b", "1.1")
package_c = get_package("c", "1.2")
repo.add_package(package_a)
installed.add_package(package_a)
repo.add_package(package_b)
installed.add_package(package_b)
repo.add_package(package_c)
installed.add_package(package_c)
installed.add_package(package) # Root package never removed.
package.add_dependency(Factory.create_dependency(package_a.name, package_a.version))
locker.locked(True)
locker.mock_lock_data(
{
"package": [
{
"name": package_a.name,
"version": package_a.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_b.name,
"version": package_b.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_c.name,
"version": package_c.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {package_a.name: [], package_b.name: [], package_c.name: []},
},
}
)
installer.run()
assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count
assert 0 == installer.executor.removals_count
def test_run_install_removes_locked_packages_if_installed_and_synchronization_is_required(
installer, locker, repo, package, installed
):
package_a = get_package("a", "1.0")
package_b = get_package("b", "1.1")
package_c = get_package("c", "1.2")
repo.add_package(package_a)
installed.add_package(package_a)
repo.add_package(package_b)
installed.add_package(package_b)
repo.add_package(package_c)
installed.add_package(package_c)
installed.add_package(package) # Root package never removed.
package.add_dependency(Factory.create_dependency(package_a.name, package_a.version))
locker.locked(True)
locker.mock_lock_data(
{
"package": [
{
"name": package_a.name,
"version": package_a.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_b.name,
"version": package_b.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_c.name,
"version": package_c.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {package_a.name: [], package_b.name: [], package_c.name: []},
},
}
)
installer.requires_synchronization(True)
installer.run()
assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count
assert 2 == installer.executor.removals_count
def test_run_install_removes_no_longer_locked_packages_if_installed(
installer, locker, repo, package, installed
):
package_a = get_package("a", "1.0")
package_b = get_package("b", "1.1")
package_c = get_package("c", "1.2")
repo.add_package(package_a)
installed.add_package(package_a)
repo.add_package(package_b)
installed.add_package(package_b)
repo.add_package(package_c)
installed.add_package(package_c)
installed.add_package(package) # Root package never removed.
package.add_dependency(Factory.create_dependency(package_a.name, package_a.version))
locker.locked(True)
locker.mock_lock_data(
{
"package": [
{
"name": package_a.name,
"version": package_a.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_b.name,
"version": package_b.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
{
"name": package_c.name,
"version": package_c.version.text,
"category": "main",
"optional": False,
"platform": "*",
"python-versions": "*",
"checksum": [],
},
],
"metadata": {
"python-versions": "*",
"platform": "*",
"content-hash": "123456789",
"hashes": {package_a.name: [], package_b.name: [], package_c.name: []},
},
}
)
installer.update(True)
installer.run()
assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count
assert 2 == installer.executor.removals_count
def test_run_install_with_optional_group_selected( def test_run_install_with_optional_group_selected(
...@@ -406,7 +606,7 @@ def test_run_install_with_optional_group_selected( ...@@ -406,7 +606,7 @@ def test_run_install_with_optional_group_selected(
) )
], ],
) )
def test_run_install_remove_untracked( def test_run_install_with_synchronization(
managed_reserved_package_names, installer, locker, repo, package, installed managed_reserved_package_names, installer, locker, repo, package, installed
): ):
package_a = get_package("a", "1.0") package_a = get_package("a", "1.0")
...@@ -462,7 +662,7 @@ def test_run_install_remove_untracked( ...@@ -462,7 +662,7 @@ def test_run_install_remove_untracked(
} }
) )
installer.remove_untracked(True) installer.requires_synchronization(True)
installer.run() installer.run()
assert 0 == installer.executor.installations_count assert 0 == installer.executor.installations_count
......
...@@ -293,7 +293,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed): ...@@ -293,7 +293,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed):
assert len(updates) == 0 assert len(updates) == 0
removals = installer.installer.removals removals = installer.installer.removals
assert len(removals) == 1 assert len(removals) == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
...@@ -308,7 +308,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed): ...@@ -308,7 +308,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed):
) )
], ],
) )
def test_run_install_remove_untracked( def test_run_install_with_synchronization(
managed_reserved_package_names, installer, locker, repo, package, installed managed_reserved_package_names, installer, locker, repo, package, installed
): ):
package_a = get_package("a", "1.0") package_a = get_package("a", "1.0")
...@@ -364,7 +364,7 @@ def test_run_install_remove_untracked( ...@@ -364,7 +364,7 @@ def test_run_install_remove_untracked(
} }
) )
installer.remove_untracked(True) installer.requires_synchronization(True)
installer.run() installer.run()
installs = installer.installer.installs installs = installer.installer.installs
...@@ -374,6 +374,7 @@ def test_run_install_remove_untracked( ...@@ -374,6 +374,7 @@ def test_run_install_remove_untracked(
assert len(updates) == 0 assert len(updates) == 0
removals = installer.installer.removals removals = installer.installer.removals
expected_removals = { expected_removals = {
package_b.name, package_b.name,
package_c.name, package_c.name,
......
from poetry.core.packages.package import Package
from poetry.puzzle.transaction import Transaction
def check_operations(ops, expected):
for e in expected:
if "skipped" not in e:
e["skipped"] = False
result = []
for op in ops:
if "update" == op.job_type:
result.append(
{
"job": "update",
"from": op.initial_package,
"to": op.target_package,
"skipped": op.skipped,
}
)
else:
job = "install"
if op.job_type == "uninstall":
job = "remove"
result.append({"job": job, "package": op.package, "skipped": op.skipped})
assert expected == result
def test_it_should_calculate_operations_in_correct_order():
transaction = Transaction(
[Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")],
[
(Package("a", "1.0.0"), 1),
(Package("b", "2.1.0"), 2),
(Package("d", "4.0.0"), 0),
],
)
check_operations(
transaction.calculate_operations(),
[
{"job": "install", "package": Package("b", "2.1.0")},
{"job": "install", "package": Package("a", "1.0.0")},
{"job": "install", "package": Package("d", "4.0.0")},
],
)
def test_it_should_calculate_operations_for_installed_packages():
transaction = Transaction(
[Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")],
[
(Package("a", "1.0.0"), 1),
(Package("b", "2.1.0"), 2),
(Package("d", "4.0.0"), 0),
],
installed_packages=[
Package("a", "1.0.0"),
Package("b", "2.0.0"),
Package("c", "3.0.0"),
Package("e", "5.0.0"),
],
)
check_operations(
transaction.calculate_operations(),
[
{"job": "remove", "package": Package("c", "3.0.0")},
{
"job": "update",
"from": Package("b", "2.0.0"),
"to": Package("b", "2.1.0"),
},
{"job": "install", "package": Package("a", "1.0.0"), "skipped": True},
{"job": "install", "package": Package("d", "4.0.0")},
],
)
def test_it_should_remove_installed_packages_if_required():
transaction = Transaction(
[Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")],
[
(Package("a", "1.0.0"), 1),
(Package("b", "2.1.0"), 2),
(Package("d", "4.0.0"), 0),
],
installed_packages=[
Package("a", "1.0.0"),
Package("b", "2.0.0"),
Package("c", "3.0.0"),
Package("e", "5.0.0"),
],
)
check_operations(
transaction.calculate_operations(synchronize=True),
[
{"job": "remove", "package": Package("c", "3.0.0")},
{"job": "remove", "package": Package("e", "5.0.0")},
{
"job": "update",
"from": Package("b", "2.0.0"),
"to": Package("b", "2.1.0"),
},
{"job": "install", "package": Package("a", "1.0.0"), "skipped": True},
{"job": "install", "package": Package("d", "4.0.0")},
],
)
def test_it_should_update_installed_packages_if_sources_are_different():
transaction = Transaction(
[Package("a", "1.0.0")],
[
(
Package(
"a",
"1.0.0",
source_url="https://github.com/demo/demo.git",
source_type="git",
source_reference="main",
source_resolved_reference="123456",
),
1,
)
],
installed_packages=[Package("a", "1.0.0")],
)
check_operations(
transaction.calculate_operations(synchronize=True),
[
{
"job": "update",
"from": Package("a", "1.0.0"),
"to": Package(
"a",
"1.0.0",
source_url="https://github.com/demo/demo.git",
source_type="git",
source_reference="main",
source_resolved_reference="123456",
),
}
],
)
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