From ea8fb8c6679f1b9afee4ef141467acc615eca71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Tue, 13 Jul 2021 18:13:08 +0200 Subject: [PATCH] Improve package operations management --- docs/cli.md | 22 +- docs/managing-dependencies.md | 26 ++ poetry/console/commands/debug/resolve.py | 4 +- poetry/console/commands/install.py | 16 +- poetry/console/commands/show.py | 2 +- poetry/installation/installer.py | 46 ++- poetry/puzzle/solver.py | 153 +-------- poetry/puzzle/transaction.py | 113 +++++++ tests/console/commands/test_install.py | 11 + tests/installation/test_installer.py | 210 ++++++++++++- tests/installation/test_installer_old.py | 7 +- tests/puzzle/test_solver.py | 383 ++++++++++++----------- tests/puzzle/test_transaction.py | 149 +++++++++ 13 files changed, 777 insertions(+), 365 deletions(-) create mode 100644 poetry/puzzle/transaction.py create mode 100644 tests/puzzle/test_transaction.py 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..d70dc1e934b 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..89cb8a2ddef 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, synchronize=False): for e in expected: if "skipped" not in e: e["skipped"] = False result = [] + ops = transaction.calculate_operations(synchronize=synchronize) 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}, @@ -2437,27 +2440,29 @@ 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) +def test_solver_synchronize_single(package, pool, installed, locked, io): + 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}], synchronize=True + ) @pytest.mark.skip(reason="Poetry no longer has critical package requirements") -def test_solver_remove_untracked_keeps_critical_package( +def test_solver_with_synchronization_keeps_critical_package( package, pool, installed, locked, io ): - solver = Solver(package, pool, installed, locked, io, remove_untracked=True) + solver = Solver(package, pool, installed, locked, io) 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..8799ea9ffab --- /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(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", + ), + } + ], + )