diff --git a/docs/cli.md b/docs/cli.md
index 62f4be8af0e..e134569ca17 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -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
about dependency groups.
-If you want to remove old dependencies no longer present in the lock file, use the
-`--remove-untracked` option.
+If you want to synchronize your environment – and ensure it matches the lock file – use the
+`--sync` option.
```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
@@ -204,12 +212,14 @@ option is used.
* `--with`: The optional dependency groups to include for installation.
* `--only`: The only dependency groups to install.
* `--default`: Only install the default dependencies.
-* `--no-dev`: Do not install dev dependencies. (**Deprecated**)
-* `--dev-only`: Only install dev dependencies. (**Deprecated**)
+* `--sync`: Synchronize the environment with the locked packages and the specified groups.
* `--no-root`: Do not install the root package (your project).
* `--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).
+* `--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
diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md
index 8b5fbdb4a46..a03e47b9510 100644
--- a/docs/managing-dependencies.md
+++ b/docs/managing-dependencies.md
@@ -146,3 +146,29 @@ to remove packages from a specific group:
```bash
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 %}}
diff --git a/poetry/console/commands/debug/resolve.py b/poetry/console/commands/debug/resolve.py
index c21fc1cb011..9d00aec51bb 100644
--- a/poetry/console/commands/debug/resolve.py
+++ b/poetry/console/commands/debug/resolve.py
@@ -86,7 +86,7 @@ def handle(self) -> Optional[int]:
solver = Solver(package, pool, Repository(), Repository(), self._io)
- ops = solver.solve()
+ ops = solver.solve().calculate_operations()
self.line("")
self.line("Resolution results:")
@@ -123,7 +123,7 @@ def handle(self) -> Optional[int]:
solver = Solver(package, pool, Repository(), Repository(), NullIO())
with solver.use_environment(env):
- ops = solver.solve()
+ ops = solver.solve().calculate_operations()
for op in ops:
if self.option("install") and op.skipped:
diff --git a/poetry/console/commands/install.py b/poetry/console/commands/install.py
index e5416e0cd78..40882394e22 100644
--- a/poetry/console/commands/install.py
+++ b/poetry/console/commands/install.py
@@ -41,6 +41,11 @@ class InstallCommand(InstallerCommand):
None,
"Only install the development dependencies. (Deprecated)",
),
+ 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)."
),
@@ -138,11 +143,20 @@ def handle(self) -> int:
if self.option("default"):
only_groups.append("default")
+ with_synchronization = self.option("sync")
+ if self.option("remove-untracked"):
+ self.line(
+ "The `--remove-untracked>` option is deprecated,"
+ "use the `--sync>` option instead."
+ )
+
+ with_synchronization = True
+
self._installer.only_groups(only_groups)
self._installer.without_groups(excluded_groups)
self._installer.with_groups(included_groups)
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())
return_code = self._installer.run()
diff --git a/poetry/console/commands/show.py b/poetry/console/commands/show.py
index 1ed8586940d..30c240ea90e 100644
--- a/poetry/console/commands/show.py
+++ b/poetry/console/commands/show.py
@@ -161,7 +161,7 @@ def handle(self) -> Optional[int]:
)
solver.provider.load_deferred(False)
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])
diff --git a/poetry/installation/installer.py b/poetry/installation/installer.py
index 91ea47961db..13cd3328598 100644
--- a/poetry/installation/installer.py
+++ b/poetry/installation/installer.py
@@ -50,7 +50,7 @@ def __init__(
self._pool = pool
self._dry_run = False
- self._remove_untracked = False
+ self._requires_synchronization = False
self._update = False
self._verbose = False
self._write_lock = True
@@ -122,14 +122,13 @@ def dry_run(self, dry_run: bool = True) -> "Installer":
def is_dry_run(self) -> bool:
return self._dry_run
- def remove_untracked(self, remove_untracked: bool = True) -> "Installer":
- self._remove_untracked = remove_untracked
+ def requires_synchronization(
+ self, requires_synchronization: bool = True
+ ) -> "Installer":
+ self._requires_synchronization = requires_synchronization
return self
- def is_remove_untracked(self) -> bool:
- return self._remove_untracked
-
def verbose(self, verbose: bool = True) -> "Installer":
self._verbose = verbose
self._executor.verbose(verbose)
@@ -212,7 +211,7 @@ def _do_refresh(self) -> int:
self._io,
)
- ops = solver.solve(use_latest=[])
+ ops = solver.solve(use_latest=[]).calculate_operations()
local_repo = Repository()
self._populate_local_repo(local_repo, ops)
@@ -247,10 +246,9 @@ def _do_install(self, local_repo: Repository) -> int:
self._installed_repository,
locked_repository,
self._io,
- remove_untracked=self._remove_untracked,
)
- ops = solver.solve(use_latest=self._whitelist)
+ ops = solver.solve(use_latest=self._whitelist).calculate_operations()
else:
self._io.write_line("Installing dependencies from lock file>")
@@ -318,19 +316,35 @@ def _do_install(self, local_repo: Repository) -> int:
pool.add_repository(repo)
solver = Solver(
- root,
- pool,
- self._installed_repository,
- locked_repository,
- NullIO(),
- remove_untracked=self._remove_untracked,
+ root, pool, self._installed_repository, locked_repository, NullIO()
)
# Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies)
solver.provider.load_deferred(False)
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
# not compatible with the current system,
diff --git a/poetry/puzzle/solver.py b/poetry/puzzle/solver.py
index e46a77c8589..28cb3653c39 100644
--- a/poetry/puzzle/solver.py
+++ b/poetry/puzzle/solver.py
@@ -16,9 +16,6 @@
from poetry.core.packages.package import Package
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.failure import SolveFailure
from poetry.packages import DependencyPackage
@@ -37,7 +34,8 @@
from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.url_dependency import URLDependency
from poetry.core.packages.vcs_dependency import VCSDependency
- from poetry.installation.operations import OperationTypes
+
+ from .transaction import Transaction
class Solver:
@@ -48,7 +46,6 @@ def __init__(
installed: Repository,
locked: Repository,
io: IO,
- remove_untracked: bool = False,
provider: Optional[Provider] = None,
):
self._package = package
@@ -62,39 +59,19 @@ def __init__(
self._provider = provider
self._overrides = []
- self._remove_untracked = remove_untracked
-
- self._preserved_package_names = None
@property
def provider(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
def use_environment(self, env: Env) -> None:
with self.provider.use_environment(env):
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():
start = time.time()
packages, depths = self._solve(use_latest=use_latest)
@@ -110,121 +87,11 @@ def solve(self, use_latest: List[str] = None) -> List["OperationTypes"]:
f"Resolved with overrides: {', '.join(f'({b})' for b in self._overrides)}"
)
- operations = []
- for i, package in enumerate(packages):
- installed = False
- for pkg in self._installed.packages:
- if package.name == pkg.name:
- 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,
- ),
+ return Transaction(
+ self._locked.packages,
+ list(zip(packages, depths)),
+ installed_packages=self._installed.packages,
+ root_package=self._package,
)
def solve_in_compatibility_mode(
diff --git a/poetry/puzzle/transaction.py b/poetry/puzzle/transaction.py
new file mode 100644
index 00000000000..91983379c60
--- /dev/null
+++ b/poetry/puzzle/transaction.py
@@ -0,0 +1,113 @@
+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,
+ ),
+ )
diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py
index 5551681854e..ac6bd21d705 100644
--- a/tests/console/commands/test_install.py
+++ b/tests/console/commands/test_install.py
@@ -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._without_groups == ["baz", "bim"]
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
diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py
index 4109f2a9430..e7deba08232 100644
--- a/tests/installation/test_installer.py
+++ b/tests/installation/test_installer.py
@@ -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.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):
@@ -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.updates_count
- assert 2 == installer.executor.removals_count
+ assert 0 == installer.executor.removals_count
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.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(
@@ -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
):
package_a = get_package("a", "1.0")
@@ -462,7 +662,7 @@ def test_run_install_remove_untracked(
}
)
- installer.remove_untracked(True)
+ installer.requires_synchronization(True)
installer.run()
assert 0 == installer.executor.installations_count
diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py
index 5c26f641598..0ce71122a7d 100644
--- a/tests/installation/test_installer_old.py
+++ b/tests/installation/test_installer_old.py
@@ -293,7 +293,7 @@ def test_run_install_no_group(installer, locker, repo, package, installed):
assert len(updates) == 0
removals = installer.installer.removals
- assert len(removals) == 1
+ assert len(removals) == 0
@pytest.mark.parametrize(
@@ -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
):
package_a = get_package("a", "1.0")
@@ -364,7 +364,7 @@ def test_run_install_remove_untracked(
}
)
- installer.remove_untracked(True)
+ installer.requires_synchronization(True)
installer.run()
installs = installer.installer.installs
@@ -374,6 +374,7 @@ def test_run_install_remove_untracked(
assert len(updates) == 0
removals = installer.installer.removals
+
expected_removals = {
package_b.name,
package_c.name,
diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py
index 0eee73fcb67..c43a2e145f0 100644
--- a/tests/puzzle/test_solver.py
+++ b/tests/puzzle/test_solver.py
@@ -67,12 +67,13 @@ def solver(package, pool, installed, locked, io):
)
-def check_solver_result(ops, expected):
+def check_solver_result(transaction, expected, remove_untracked=False):
for e in expected:
if "skipped" not in e:
e["skipped"] = False
result = []
+ ops = transaction.calculate_operations(remove_untracked=remove_untracked)
for op in ops:
if "update" == op.job_type:
result.append(
@@ -92,6 +93,8 @@ def check_solver_result(ops, expected):
assert expected == result
+ return ops
+
def test_solver_install_single(solver, repo, package):
package.add_dependency(Factory.create_dependency("A", "*"))
@@ -99,9 +102,9 @@ def test_solver_install_single(solver, repo, package):
package_a = get_package("A", "1.0")
repo.add_package(package_a)
- ops = solver.solve([get_dependency("A")])
+ transaction = solver.solve([get_dependency("A")])
- check_solver_result(ops, [{"job": "install", "package": package_a}])
+ check_solver_result(transaction, [{"job": "install", "package": package_a}])
def test_solver_remove_if_no_longer_locked(solver, locked, installed):
@@ -109,9 +112,9 @@ def test_solver_remove_if_no_longer_locked(solver, locked, installed):
installed.add_package(package_a)
locked.add_package(package_a)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "remove", "package": package_a}])
+ check_solver_result(transaction, [{"job": "remove", "package": package_a}])
def test_remove_non_installed(solver, repo, locked):
@@ -122,9 +125,9 @@ def test_remove_non_installed(solver, repo, locked):
request = []
- ops = solver.solve(request)
+ transaction = solver.solve(request)
- check_solver_result(ops, [{"job": "remove", "package": package_a, "skipped": True}])
+ check_solver_result(transaction, [])
def test_install_non_existing_package_fail(solver, repo, package):
@@ -150,10 +153,10 @@ def test_solver_with_deps(solver, repo, package):
package_a.add_dependency(get_dependency("B", "<1.1"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
@@ -178,10 +181,10 @@ def test_install_honours_not_equal(solver, repo, package):
package_a.add_dependency(get_dependency("B", "<=1.3,!=1.3,!=1.2"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": new_package_b11},
{"job": "install", "package": package_a},
@@ -206,10 +209,10 @@ def test_install_with_deps_in_order(solver, repo, package):
package_c.add_dependency(get_dependency("A", ">=1.0"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_c},
@@ -225,10 +228,10 @@ def test_install_installed(solver, repo, installed, package):
installed.add_package(package_a)
repo.add_package(package_a)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops, [{"job": "install", "package": package_a, "skipped": True}]
+ transaction, [{"job": "install", "package": package_a, "skipped": True}]
)
@@ -242,10 +245,10 @@ def test_update_installed(solver, repo, installed, package):
repo.add_package(package_a)
repo.add_package(new_package_a)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops, [{"job": "update", "from": package_a, "to": new_package_a}]
+ transaction, [{"job": "update", "from": package_a, "to": new_package_a}]
)
@@ -267,10 +270,10 @@ def test_update_with_use_latest(solver, repo, installed, package, locked):
locked.add_package(package_a)
locked.add_package(package_b)
- ops = solver.solve(use_latest=[package_b.name])
+ transaction = solver.solve(use_latest=[package_b.name])
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a, "skipped": True},
{"job": "install", "package": new_package_b},
@@ -291,10 +294,10 @@ def test_solver_sets_groups(solver, repo, package):
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_a},
@@ -326,10 +329,10 @@ def test_solver_respects_root_package_python_versions(solver, repo, package):
repo.add_package(package_c)
repo.add_package(package_c11)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_a},
@@ -379,10 +382,10 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_a},
@@ -405,10 +408,10 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo, package):
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_b},
@@ -435,10 +438,10 @@ def test_solver_returns_extras_if_requested(solver, repo, package):
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_a},
@@ -482,7 +485,7 @@ def test_solver_returns_extras_only_requested(solver, repo, package, enabled_ext
repo.add_package(package_c10)
repo.add_package(package_c20)
- ops = solver.solve()
+ transaction = solver.solve()
expected = [
{"job": "install", "package": package_a},
@@ -498,8 +501,8 @@ def test_solver_returns_extras_only_requested(solver, repo, package, enabled_ext
},
)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
expected,
)
@@ -534,7 +537,7 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency(
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
expected = [
{"job": "install", "package": package_b},
@@ -544,8 +547,8 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency(
if enabled_extra is not None:
expected.insert(0, {"job": "install", "package": package_c})
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
expected,
)
@@ -587,7 +590,7 @@ def test_solver_returns_extras_only_requested_nested(
repo.add_package(package_c10)
repo.add_package(package_c20)
- ops = solver.solve()
+ transaction = solver.solve()
expected = [
{"job": "install", "package": package_b},
@@ -603,7 +606,7 @@ def test_solver_returns_extras_only_requested_nested(
},
)
- check_solver_result(ops, expected)
+ ops = check_solver_result(transaction, expected)
assert ops[-1].package.marker.is_any()
assert ops[0].package.marker.is_any()
@@ -626,10 +629,10 @@ def test_solver_returns_prereleases_if_requested(solver, repo, package):
repo.add_package(package_c)
repo.add_package(package_c_dev)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_b},
@@ -653,10 +656,10 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo, packa
repo.add_package(package_c)
repo.add_package(package_c_dev)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_b},
@@ -685,10 +688,10 @@ def test_solver_sub_dependencies_with_requirements(solver, repo, package):
repo.add_package(package_c)
repo.add_package(package_d)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_d},
{"job": "install", "package": package_c},
@@ -743,10 +746,10 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package
repo.add_package(package_e)
repo.add_package(package_f)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_e},
{"job": "install", "package": package_f},
@@ -775,9 +778,9 @@ def test_solver_sub_dependencies_with_not_supported_python_version(
repo.add_package(package_a)
repo.add_package(package_b)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "install", "package": package_a}])
+ check_solver_result(transaction, [{"job": "install", "package": package_a}])
def test_solver_sub_dependencies_with_not_supported_python_version_transitive(
@@ -812,10 +815,10 @@ def test_solver_sub_dependencies_with_not_supported_python_version_transitive(
repo.add_package(sniffio)
repo.add_package(sniffio_1_1_0)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": sniffio, "skipped": False},
{"job": "install", "package": httpcore, "skipped": False},
@@ -854,10 +857,10 @@ def test_solver_with_dependency_in_both_default_and_dev_dependencies(
repo.add_package(package_c)
repo.add_package(package_d)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_d},
{"job": "install", "package": package_b},
@@ -911,10 +914,10 @@ def test_solver_with_dependency_in_both_main_and_dev_dependencies_with_one_more_
repo.add_package(package_d)
repo.add_package(package_e)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_d},
@@ -951,10 +954,10 @@ def test_solver_with_dependency_and_prerelease_sub_dependencies(solver, repo, pa
package_b = get_package("B", "1.0.0.dev4")
repo.add_package(package_b)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
@@ -978,10 +981,10 @@ def test_solver_circular_dependency(solver, repo, package):
repo.add_package(package_b)
repo.add_package(package_c)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_b},
@@ -1012,10 +1015,10 @@ def test_solver_circular_dependency_chain(solver, repo, package):
repo.add_package(package_c)
repo.add_package(package_d)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_d},
{"job": "install", "package": package_c},
@@ -1041,10 +1044,10 @@ def test_solver_dense_dependencies(solver, repo, package):
for j in range(i):
package_ai.add_dependency(Factory.create_dependency("a" + str(j), "^1.0"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops, [{"job": "install", "package": packages[i]} for i in range(n)]
+ transaction, [{"job": "install", "package": packages[i]} for i in range(n)]
)
@@ -1064,10 +1067,10 @@ def test_solver_duplicate_dependencies_same_constraint(solver, repo, package):
repo.add_package(package_a)
repo.add_package(package_b)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
@@ -1093,10 +1096,10 @@ def test_solver_duplicate_dependencies_different_constraints(solver, repo, packa
repo.add_package(package_b10)
repo.add_package(package_b20)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b10},
{"job": "install", "package": package_b20},
@@ -1157,10 +1160,10 @@ def test_solver_duplicate_dependencies_sub_dependencies(solver, repo, package):
repo.add_package(package_c12)
repo.add_package(package_c15)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_c12},
{"job": "install", "package": package_c15},
@@ -1198,10 +1201,10 @@ def test_solver_does_not_get_stuck_in_recursion_on_circular_dependency(
package.add_dependency(Factory.create_dependency("A", "^1.0"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_b},
@@ -1220,7 +1223,7 @@ def test_solver_can_resolve_git_dependencies(solver, repo, package):
Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"})
)
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package(
"demo",
@@ -1231,8 +1234,8 @@ def test_solver_can_resolve_git_dependencies(solver, repo, package):
source_resolved_reference="9cf87a285a2d3fbb0b9fa621997b3acc3631ed24",
)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[{"job": "install", "package": pendulum}, {"job": "install", "package": demo}],
)
@@ -1255,7 +1258,7 @@ def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package):
)
)
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package(
"demo",
@@ -1267,7 +1270,7 @@ def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package):
)
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": cleo},
{"job": "install", "package": pendulum},
@@ -1300,10 +1303,10 @@ def test_solver_can_resolve_git_dependencies_with_ref(solver, repo, package, ref
git_config.update(ref)
package.add_dependency(Factory.create_dependency("demo", git_config))
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[{"job": "install", "package": pendulum}, {"job": "install", "package": demo}],
)
@@ -1327,9 +1330,9 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir
repo.add_package(package_a)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "install", "package": package_a}])
+ check_solver_result(transaction, [{"job": "install", "package": package_a}])
def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible_multiple(
@@ -1353,10 +1356,10 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir
repo.add_package(package_a)
repo.add_package(package_b)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
@@ -1398,9 +1401,9 @@ def test_solver_finds_compatible_package_for_dependency_python_not_fully_compati
repo.add_package(package_a100)
repo.add_package(package_a101)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "install", "package": package_a100}])
+ check_solver_result(transaction, [{"job": "install", "package": package_a100}])
def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_only_extras(
@@ -1427,10 +1430,10 @@ def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_onl
repo.add_package(package_b1)
repo.add_package(package_b2)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": package_b2},
{"job": "install", "package": package_a},
@@ -1462,10 +1465,10 @@ def test_solver_does_not_raise_conflict_for_locked_conditional_dependencies(
repo.add_package(package_b)
solver._locked = Repository([package_a])
- ops = solver.solve(use_latest=[package_b.name])
+ transaction = solver.solve(use_latest=[package_b.name])
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_b},
@@ -1499,10 +1502,10 @@ def test_solver_returns_extras_if_requested_in_dependencies_and_not_in_root_pack
repo.add_package(package_c)
repo.add_package(package_d)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_d},
{"job": "install", "package": package_c},
@@ -1552,10 +1555,10 @@ def test_solver_ignores_dependencies_with_incompatible_python_full_version_marke
repo.add_package(package_b100)
repo.add_package(package_b200)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a},
{"job": "install", "package": package_b200},
@@ -1591,10 +1594,10 @@ def test_solver_git_dependencies_update(solver, repo, package, installed):
Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"})
)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": pendulum},
{"job": "update", "from": demo_installed, "to": demo},
@@ -1630,10 +1633,10 @@ def test_solver_git_dependencies_update_skipped(solver, repo, package, installed
Factory.create_dependency("demo", {"git": "https://github.com/demo/demo.git"})
)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": pendulum},
{"job": "install", "package": demo, "skipped": True},
@@ -1654,7 +1657,7 @@ def test_solver_git_dependencies_short_hash_update_skipped(
"0.1.2",
source_type="git",
source_url="https://github.com/demo/demo.git",
- source_reference="master",
+ source_reference="9cf87a285a2d3fbb0b9fa621997b3acc3631ed24",
source_resolved_reference="9cf87a285a2d3fbb0b9fa621997b3acc3631ed24",
)
installed.add_package(demo)
@@ -1665,10 +1668,10 @@ def test_solver_git_dependencies_short_hash_update_skipped(
)
)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": pendulum},
{
@@ -1702,12 +1705,12 @@ def test_solver_can_resolve_directory_dependencies(solver, repo, package):
package.add_dependency(Factory.create_dependency("demo", {"path": path}))
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.2", source_type="directory", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[{"job": "install", "package": pendulum}, {"job": "install", "package": demo}],
)
@@ -1730,10 +1733,10 @@ def test_solver_can_resolve_directory_dependencies_nested_editable(
package, pool, installed, locked, io, provider=Provider(package, pool, io)
)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -1792,12 +1795,12 @@ def test_solver_can_resolve_directory_dependencies_with_extras(solver, repo, pac
Factory.create_dependency("demo", {"path": path, "extras": ["foo"]})
)
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.2", source_type="directory", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": cleo},
{"job": "install", "package": pendulum},
@@ -1826,12 +1829,12 @@ def test_solver_can_resolve_sdist_dependencies(solver, repo, package):
package.add_dependency(Factory.create_dependency("demo", {"path": path}))
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.0", source_type="file", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[{"job": "install", "package": pendulum}, {"job": "install", "package": demo}],
)
@@ -1860,12 +1863,12 @@ def test_solver_can_resolve_sdist_dependencies_with_extras(solver, repo, package
Factory.create_dependency("demo", {"path": path, "extras": ["foo"]})
)
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.0", source_type="file", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": cleo},
{"job": "install", "package": pendulum},
@@ -1894,12 +1897,12 @@ def test_solver_can_resolve_wheel_dependencies(solver, repo, package):
package.add_dependency(Factory.create_dependency("demo", {"path": path}))
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.0", source_type="file", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[{"job": "install", "package": pendulum}, {"job": "install", "package": demo}],
)
@@ -1928,12 +1931,12 @@ def test_solver_can_resolve_wheel_dependencies_with_extras(solver, repo, package
Factory.create_dependency("demo", {"path": path, "extras": ["foo"]})
)
- ops = solver.solve()
+ transaction = solver.solve()
demo = Package("demo", "0.1.0", source_type="file", source_url=path)
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{"job": "install", "package": cleo},
{"job": "install", "package": pendulum},
@@ -1959,10 +1962,10 @@ def test_solver_can_solve_with_legacy_repository_using_proper_dists(
package.add_dependency(Factory.create_dependency("isort", "4.3.4"))
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -2003,10 +2006,10 @@ def test_solver_can_solve_with_legacy_repository_using_proper_python_compatible_
package.add_dependency(Factory.create_dependency("isort", "4.3.4"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{
"job": "install",
@@ -2032,10 +2035,10 @@ def test_solver_skips_invalid_versions(package, installed, locked, io):
package.add_dependency(Factory.create_dependency("trackpy", "^0.4"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops, [{"job": "install", "package": get_package("trackpy", "0.4.1")}]
+ transaction, [{"job": "install", "package": get_package("trackpy", "0.4.1")}]
)
@@ -2053,10 +2056,10 @@ def test_multiple_constraints_on_root(package, solver, repo):
repo.add_package(foo15)
repo.add_package(foo25)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[{"job": "install", "package": foo15}, {"job": "install", "package": foo25}],
)
@@ -2072,10 +2075,10 @@ def test_solver_chooses_most_recent_version_amongst_repositories(
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops, [{"job": "install", "package": get_package("tomlkit", "0.5.3")}]
+ ops = check_solver_result(
+ transaction, [{"job": "install", "package": get_package("tomlkit", "0.5.3")}]
)
assert ops[0].package.source_type is None
@@ -2095,10 +2098,10 @@ def test_solver_chooses_from_correct_repository_if_forced(
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -2133,10 +2136,10 @@ def test_solver_chooses_from_correct_repository_if_forced_and_transitive_depende
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -2170,10 +2173,10 @@ def test_solver_does_not_choose_from_secondary_repository_by_default(
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -2217,10 +2220,10 @@ def test_solver_chooses_from_secondary_if_explicit(package, installed, locked, i
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(
- ops,
+ ops = check_solver_result(
+ transaction,
[
{
"job": "install",
@@ -2269,10 +2272,10 @@ def test_solver_discards_packages_with_empty_markers(
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_c},
{"job": "install", "package": package_a},
@@ -2301,10 +2304,10 @@ def test_solver_does_not_raise_conflict_for_conditional_dev_dependencies(
repo.add_package(package_a100)
repo.add_package(package_a200)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a100},
{"job": "install", "package": package_a200},
@@ -2335,10 +2338,10 @@ def test_solver_does_not_loop_indefinitely_on_duplicate_constraints_with_extras(
repo.add_package(requests)
repo.add_package(idna)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[{"job": "install", "package": idna}, {"job": "install", "package": requests}],
)
@@ -2370,10 +2373,10 @@ def test_solver_does_not_fail_with_locked_git_and_non_git_dependencies(
solver = Solver(package, pool, installed, locked, io)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": get_package("a", "1.2.3")},
{"job": "install", "package": git_package, "skipped": True},
@@ -2396,10 +2399,10 @@ def test_ignore_python_constraint_no_overlap_dependencies(solver, repo, package)
repo.add_package(pytest)
repo.add_package(get_package("configparser", "1.2.3"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[{"job": "install", "package": pytest}],
)
@@ -2425,10 +2428,10 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies(
repo.add_package(package_b10)
repo.add_package(package_b20)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_b10},
{"job": "install", "package": package_b20},
@@ -2438,13 +2441,15 @@ def test_solver_should_not_go_into_an_infinite_loop_on_duplicate_dependencies(
def test_solver_remove_untracked_single(package, pool, installed, locked, io):
- solver = Solver(package, pool, installed, locked, io, remove_untracked=True)
+ solver = Solver(package, pool, installed, locked, io)
package_a = get_package("a", "1.0")
installed.add_package(package_a)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "remove", "package": package_a}])
+ check_solver_result(
+ transaction, [{"job": "remove", "package": package_a}], remove_untracked=True
+ )
@pytest.mark.skip(reason="Poetry no longer has critical package requirements")
@@ -2455,9 +2460,9 @@ def test_solver_remove_untracked_keeps_critical_package(
package_pip = get_package("setuptools", "1.0")
installed.add_package(package_pip)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [])
+ check_solver_result(transaction, [])
def test_solver_cannot_choose_another_version_for_directory_dependencies(
@@ -2591,9 +2596,11 @@ def test_solver_should_not_update_same_version_packages_if_installed_has_no_sour
repo.add_package(foo)
installed.add_package(get_package("foo", "1.0.0"))
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "install", "package": foo, "skipped": True}])
+ check_solver_result(
+ transaction, [{"job": "install", "package": foo, "skipped": True}]
+ )
def test_solver_should_use_the_python_constraint_from_the_environment_if_available(
@@ -2615,10 +2622,10 @@ def test_solver_should_use_the_python_constraint_from_the_environment_if_availab
repo.add_package(b)
with solver.use_environment(MockEnv((2, 7, 18))):
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[{"job": "install", "package": b}, {"job": "install", "package": a}],
)
@@ -2658,10 +2665,10 @@ def test_solver_should_resolve_all_versions_for_multiple_duplicate_dependencies(
repo.add_package(package_b30)
repo.add_package(package_b40)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": package_a10},
{"job": "install", "package": package_a20},
@@ -2684,9 +2691,9 @@ def test_solver_should_not_raise_errors_for_irrelevant_python_constraints(
dataclasses.python_versions = ">=3.6, <3.7"
repo.add_package(dataclasses)
- ops = solver.solve()
+ transaction = solver.solve()
- check_solver_result(ops, [{"job": "install", "package": dataclasses}])
+ check_solver_result(transaction, [{"job": "install", "package": dataclasses}])
def test_solver_can_resolve_transitive_extras(solver, repo, package):
@@ -2712,10 +2719,10 @@ def test_solver_can_resolve_transitive_extras(solver, repo, package):
repo.add_package(get_package("certifi", "2017.4.17"))
repo.add_package(get_package("pyopenssl", "0.14"))
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": get_package("certifi", "2017.4.17")},
{"job": "install", "package": get_package("pyopenssl", "0.14")},
@@ -2748,10 +2755,10 @@ def test_solver_can_resolve_for_packages_with_missing_extras(solver, repo, packa
repo.add_package(boto3)
repo.add_package(requests)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": django},
{"job": "install", "package": requests},
@@ -2782,10 +2789,10 @@ def test_solver_can_resolve_python_restricted_package_dependencies(
repo.add_package(futures)
repo.add_package(pre_commit)
- ops = solver.solve(use_latest=["pre-commit"])
+ transaction = solver.solve(use_latest=["pre-commit"])
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": futures},
{"job": "install", "package": pre_commit},
@@ -2831,10 +2838,10 @@ def test_solver_should_not_raise_errors_for_irrelevant_transitive_python_constra
repo.add_package(pre_commit)
repo.add_package(importlib_resources)
repo.add_package(importlib_resources_3_2_1)
- ops = solver.solve()
+ transaction = solver.solve()
check_solver_result(
- ops,
+ transaction,
[
{"job": "install", "package": importlib_resources_3_2_1},
{"job": "install", "package": pre_commit},
diff --git a/tests/puzzle/test_transaction.py b/tests/puzzle/test_transaction.py
new file mode 100644
index 00000000000..11e9c987821
--- /dev/null
+++ b/tests/puzzle/test_transaction.py
@@ -0,0 +1,149 @@
+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(remove_untracked=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(remove_untracked=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",
+ ),
+ }
+ ],
+ )