def test_package_with_the_same_name_gives_clear_error_message( root: ProjectPackage, provider: Provider, repo: Repository): pkg_name = "a" root.add_dependency(Factory.create_dependency(pkg_name, "*")) add_to_repo(repo, pkg_name, "1.0.0", deps={pkg_name: "1.0.0"}) error = f"Package '{pkg_name}' is listed as a dependency of itself." check_solver_result(root, provider, error=error)
def test_with_compatible_locked_dependencies_use_latest( root: ProjectPackage, provider: Provider, repo: Repository ): root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("baz", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "1.0.1", deps={"bar": "1.0.1"}) add_to_repo(repo, "foo", "1.0.2", deps={"bar": "1.0.2"}) add_to_repo(repo, "bar", "1.0.0") add_to_repo(repo, "bar", "1.0.1") add_to_repo(repo, "bar", "1.0.2") add_to_repo(repo, "baz", "1.0.0") add_to_repo(repo, "baz", "1.0.1") check_solver_result( root, provider, result={"foo": "1.0.2", "bar": "1.0.2", "baz": "1.0.0"}, locked={ "foo": get_package("foo", "1.0.1"), "bar": get_package("bar", "1.0.1"), "baz": get_package("baz", "1.0.0"), }, use_latest=["foo"], )
def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_satisfied( root: ProjectPackage, provider: Provider, repo: Repository ): root.add_dependency(Factory.create_dependency("foo", "*")) root.add_dependency(Factory.create_dependency("newdep", "2.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "<2.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"baz": "<2.0.0"}) add_to_repo(repo, "baz", "1.0.0", deps={"qux": "<2.0.0"}) add_to_repo(repo, "qux", "1.0.0") add_to_repo(repo, "foo", "2.0.0", deps={"bar": "<3.0.0"}) add_to_repo(repo, "bar", "2.0.0", deps={"baz": "<3.0.0"}) add_to_repo(repo, "baz", "2.0.0", deps={"qux": "<3.0.0"}) add_to_repo(repo, "qux", "2.0.0") add_to_repo(repo, "newdep", "2.0.0", deps={"baz": ">=1.5.0"}) check_solver_result( root, provider, result={ "foo": "2.0.0", "bar": "2.0.0", "baz": "2.0.0", "qux": "1.0.0", "newdep": "2.0.0", }, locked={ "foo": get_package("foo", "2.0.0"), "bar": get_package("bar", "1.0.0"), "baz": get_package("baz", "1.0.0"), "qux": get_package("qux", "1.0.0"), }, )
def test_traverse_into_package_with_fewer_versions_first( root: ProjectPackage, provider: Provider, repo: Repository): # Dependencies are ordered so that packages with fewer versions are tried # first. Here, there are two valid solutions (either a or b must be # downgraded once). The chosen one depends on which dep is traversed first. # Since b has fewer versions, it will be traversed first, which means a will # come later. Since later selections are revised first, a gets downgraded. root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"c": "*"}) add_to_repo(repo, "a", "2.0.0", deps={"c": "*"}) add_to_repo(repo, "a", "3.0.0", deps={"c": "*"}) add_to_repo(repo, "a", "4.0.0", deps={"c": "*"}) add_to_repo(repo, "a", "5.0.0", deps={"c": "1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"c": "*"}) add_to_repo(repo, "b", "2.0.0", deps={"c": "*"}) add_to_repo(repo, "b", "3.0.0", deps={"c": "*"}) add_to_repo(repo, "b", "4.0.0", deps={"c": "2.0.0"}) add_to_repo(repo, "c", "1.0.0") add_to_repo(repo, "c", "2.0.0") check_solver_result(root, provider, { "a": "4.0.0", "b": "4.0.0", "c": "2.0.0" })
def test_with_compatible_locked_dependencies_with_extras( root: ProjectPackage, provider: Provider, repo: Repository ): root.add_dependency(Factory.create_dependency("foo", "^1.0")) package_foo_0 = get_package("foo", "1.0.0") package_foo_1 = get_package("foo", "1.0.1") bar_extra_dep = Factory.create_dependency( "bar", {"version": "^1.0", "extras": "extra"} ) for package_foo in (package_foo_0, package_foo_1): package_foo.add_dependency(bar_extra_dep) repo.add_package(package_foo) bar_deps = {"baz": {"version": "^1.0", "extras": ["extra"]}} add_to_repo(repo, "bar", "1.0.0", bar_deps) add_to_repo(repo, "bar", "1.0.1", bar_deps) add_to_repo(repo, "baz", "1.0.0") add_to_repo(repo, "baz", "1.0.1") check_solver_result( root, provider, result={"foo": "1.0.0", "bar": "1.0.0", "baz": "1.0.0"}, locked={ "foo": get_package("foo", "1.0.0"), "bar": get_package("bar", "1.0.0"), "baz": get_package("baz", "1.0.0"), }, )
def test_circular_dependency(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"foo": "1.0.0"}) check_solver_result(root, provider, {"foo": "1.0.0", "bar": "1.0.0"})
def test_circular_dependency_on_older_version(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", ">=1.0.0")) add_to_repo(repo, "a", "1.0.0") add_to_repo(repo, "a", "2.0.0", deps={"b": "1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"a": "1.0.0"}) check_solver_result(root, provider, {"a": "1.0.0"}, tries=2)
def test_disjoint_root_constraints(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("foo", "2.0.0")) add_to_repo(repo, "foo", "1.0.0") add_to_repo(repo, "foo", "2.0.0") error = """\ Because myapp depends on both foo (1.0.0) and foo (2.0.0), version solving failed.""" check_solver_result(root, provider, error=error)
def test_no_version_matching_constraint(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "^1.0")) add_to_repo(repo, "foo", "2.0.0") add_to_repo(repo, "foo", "2.1.3") check_solver_result( root, provider, error=("Because myapp depends on foo (^1.0) " "which doesn't match any versions, version solving failed."), )
def test_dependency_does_not_match_root_python_constraint( root: ProjectPackage, provider: Provider, repo: Repository): provider.set_package_python_versions("^3.6") root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", python="<3.5") error = """\ The current project's Python requirement (>=3.6,<4.0) is not compatible with some of\ the required packages Python requirement: - foo requires Python <3.5, so it will not be satisfied for Python >=3.6,<4.0 Because no versions of foo match !=1.0.0 and foo (1.0.0) requires Python <3.5, foo is forbidden. So, because myapp depends on foo (*), version solving failed.""" check_solver_result(root, provider, error=error)
def test_backjump_past_failed_package_on_disjoint_constraint( root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("foo", ">2.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"foo": "*"}) # ok add_to_repo(repo, "a", "2.0.0", deps={"foo": "<1.0.0"}) # disjoint with myapp's constraint on foo add_to_repo(repo, "foo", "2.0.0") add_to_repo(repo, "foo", "2.0.1") add_to_repo(repo, "foo", "2.0.2") add_to_repo(repo, "foo", "2.0.3") add_to_repo(repo, "foo", "2.0.4") check_solver_result(root, provider, {"a": "1.0.0", "foo": "2.0.4"})
def test_shared_dependencies_with_overlapping_constraints( root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"shared": ">=2.0.0 <4.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"shared": ">=3.0.0 <5.0.0"}) add_to_repo(repo, "shared", "2.0.0") add_to_repo(repo, "shared", "3.0.0") add_to_repo(repo, "shared", "3.6.9") add_to_repo(repo, "shared", "4.0.0") add_to_repo(repo, "shared", "5.0.0") check_solver_result(root, provider, { "a": "1.0.0", "b": "1.0.0", "shared": "3.6.9" })
def test_rolls_back_leaf_versions_first(root: ProjectPackage, provider: Provider, repo: Repository): # The latest versions of a and b disagree on c. An older version of either # will resolve the problem. This test validates that b, which is farther # in the dependency graph from myapp is downgraded first. root.add_dependency(Factory.create_dependency("a", "*")) add_to_repo(repo, "a", "1.0.0", deps={"b": "*"}) add_to_repo(repo, "a", "2.0.0", deps={"b": "*", "c": "2.0.0"}) add_to_repo(repo, "b", "1.0.0") add_to_repo(repo, "b", "2.0.0", deps={"c": "1.0.0"}) add_to_repo(repo, "c", "1.0.0") add_to_repo(repo, "c", "2.0.0") check_solver_result(root, provider, { "a": "2.0.0", "b": "1.0.0", "c": "2.0.0" })
def test_shared_dependency_where_dependent_version_affects_other_dependencies( root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "<=1.0.2")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0") add_to_repo(repo, "foo", "1.0.1", deps={"bang": "1.0.0"}) add_to_repo(repo, "foo", "1.0.2", deps={"whoop": "1.0.0"}) add_to_repo(repo, "foo", "1.0.3", deps={"zoop": "1.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"foo": "<=1.0.1"}) add_to_repo(repo, "bang", "1.0.0") add_to_repo(repo, "whoop", "1.0.0") add_to_repo(repo, "zoop", "1.0.0") check_solver_result(root, provider, { "foo": "1.0.1", "bar": "1.0.0", "bang": "1.0.0" })
def test_disjoint_constraints(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"shared": "<=2.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"shared": ">3.0.0"}) add_to_repo(repo, "shared", "2.0.0") add_to_repo(repo, "shared", "4.0.0") error = """\ Because bar (1.0.0) depends on shared (>3.0.0) and foo (1.0.0) depends on shared (<=2.0.0),\ bar (1.0.0) is incompatible with foo (1.0.0). So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed.\ """ check_solver_result(root, provider, error=error) check_solver_result(root, provider, error=error)
def test_no_valid_solution(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"b": "1.0.0"}) add_to_repo(repo, "a", "2.0.0", deps={"b": "2.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"a": "2.0.0"}) add_to_repo(repo, "b", "2.0.0", deps={"a": "1.0.0"}) error = """\ Because no versions of b match <1.0.0 || >1.0.0,<2.0.0 || >2.0.0 and b (1.0.0) depends on a (2.0.0), b (!=2.0.0) requires a (2.0.0). And because a (2.0.0) depends on b (2.0.0), b is forbidden. Because b (2.0.0) depends on a (1.0.0) which depends on b (1.0.0), b is forbidden. Thus, b is forbidden. So, because myapp depends on b (*), version solving failed.""" check_solver_result(root, provider, error=error, tries=2)
def test_diamond_dependency_graph(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "2.0.0", deps={"c": "^1.0.0"}) add_to_repo(repo, "a", "1.0.0") add_to_repo(repo, "b", "2.0.0", deps={"c": "^3.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"c": "^2.0.0"}) add_to_repo(repo, "c", "3.0.0") add_to_repo(repo, "c", "2.0.0") add_to_repo(repo, "c", "1.0.0") check_solver_result(root, provider, { "a": "1.0.0", "b": "2.0.0", "c": "3.0.0" })
def test_no_version_that_matches_combined_constraints(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("foo", "1.0.0")) root.add_dependency(Factory.create_dependency("bar", "1.0.0")) add_to_repo(repo, "foo", "1.0.0", deps={"shared": ">=2.0.0 <3.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"shared": ">=2.9.0 <4.0.0"}) add_to_repo(repo, "shared", "2.5.0") add_to_repo(repo, "shared", "3.5.0") error = """\ Because foo (1.0.0) depends on shared (>=2.0.0 <3.0.0) and no versions of shared match >=2.9.0,<3.0.0,\ foo (1.0.0) requires shared (>=2.0.0,<2.9.0). And because bar (1.0.0) depends on shared (>=2.9.0 <4.0.0),\ bar (1.0.0) is incompatible with foo (1.0.0). So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed.\ """ check_solver_result(root, provider, error=error)
def test_backjumps_after_partial_satisfier(root: ProjectPackage, provider: Provider, repo: Repository): # c 2.0.0 is incompatible with y 2.0.0 because it requires x 1.0.0, but that # requirement only exists because of both a and b. The solver should be able # to deduce c 2.0.0's incompatibility and select c 1.0.0 instead. root.add_dependency(Factory.create_dependency("c", "*")) root.add_dependency(Factory.create_dependency("y", "^2.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"x": ">=1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"x": "<2.0.0"}) add_to_repo(repo, "c", "1.0.0") add_to_repo(repo, "c", "2.0.0", deps={"a": "*", "b": "*"}) add_to_repo(repo, "x", "0.0.0") add_to_repo(repo, "x", "1.0.0", deps={"y": "1.0.0"}) add_to_repo(repo, "x", "2.0.0") add_to_repo(repo, "y", "1.0.0") add_to_repo(repo, "y", "2.0.0") check_solver_result(root, provider, {"c": "1.0.0", "y": "2.0.0"}, tries=2)
def test_simple_transitive(root: ProjectPackage, provider: Provider, repo: Repository): # Only one version of baz, so foo and bar will have to downgrade # until they reach it root.add_dependency(Factory.create_dependency("foo", "*")) add_to_repo(repo, "foo", "1.0.0", deps={"bar": "1.0.0"}) add_to_repo(repo, "foo", "2.0.0", deps={"bar": "2.0.0"}) add_to_repo(repo, "foo", "3.0.0", deps={"bar": "3.0.0"}) add_to_repo(repo, "bar", "1.0.0", deps={"baz": "*"}) add_to_repo(repo, "bar", "2.0.0", deps={"baz": "2.0.0"}) add_to_repo(repo, "bar", "3.0.0", deps={"baz": "3.0.0"}) add_to_repo(repo, "baz", "1.0.0") check_solver_result(root, provider, { "foo": "1.0.0", "bar": "1.0.0", "baz": "1.0.0" }, tries=3)
def test_simple_dependencies(root: ProjectPackage, provider: Provider, repo: Repository): root.add_dependency(Factory.create_dependency("a", "1.0.0")) root.add_dependency(Factory.create_dependency("b", "1.0.0")) add_to_repo(repo, "a", "1.0.0", deps={"aa": "1.0.0", "ab": "1.0.0"}) add_to_repo(repo, "b", "1.0.0", deps={"ba": "1.0.0", "bb": "1.0.0"}) add_to_repo(repo, "aa", "1.0.0") add_to_repo(repo, "ab", "1.0.0") add_to_repo(repo, "ba", "1.0.0") add_to_repo(repo, "bb", "1.0.0") check_solver_result( root, provider, { "a": "1.0.0", "aa": "1.0.0", "ab": "1.0.0", "b": "1.0.0", "ba": "1.0.0", "bb": "1.0.0", }, )
def test_backjump_to_nearer_unsatisfied_package(root: ProjectPackage, provider: Provider, repo: Repository): # This ensures it doesn't exhaustively search all versions of b when it's # a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We # make sure b has more versions than a so that the solver tries a first # since it sorts sibling dependencies by number of versions. root.add_dependency(Factory.create_dependency("a", "*")) root.add_dependency(Factory.create_dependency("b", "*")) add_to_repo(repo, "a", "1.0.0", deps={"c": "1.0.0"}) add_to_repo(repo, "a", "2.0.0", deps={"c": "2.0.0-1"}) add_to_repo(repo, "b", "1.0.0") add_to_repo(repo, "b", "2.0.0") add_to_repo(repo, "b", "3.0.0") add_to_repo(repo, "c", "1.0.0") check_solver_result(root, provider, { "a": "1.0.0", "b": "3.0.0", "c": "1.0.0" }, tries=2)
def handle(self) -> int: from pathlib import Path import tomlkit from cleo.io.inputs.string_input import StringInput from cleo.io.io import IO from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.semver.helpers import parse_constraint from poetry.factory import Factory from poetry.packages.project_package import ProjectPackage from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.env import EnvManager plugins = self.argument("plugins") # Plugins should be installed in the system env to be globally available system_env = EnvManager.get_system_env(naive=True) env_dir = Path(os.getenv("POETRY_HOME") or system_env.path) # We check for the plugins existence first. if env_dir.joinpath("pyproject.toml").exists(): pyproject = tomlkit.loads( env_dir.joinpath("pyproject.toml").read_text(encoding="utf-8")) poetry_content = pyproject["tool"]["poetry"] existing_packages = self.get_existing_packages_from_input( plugins, poetry_content, "dependencies") if existing_packages: self.notify_about_existing_packages(existing_packages) plugins = [ plugin for plugin in plugins if plugin not in existing_packages ] if not plugins: return 0 plugins = self._determine_requirements(plugins) # We retrieve the packages installed in the system environment. # We assume that this environment will be a self contained virtual environment # built by the official installer or by pipx. # If not, it might lead to side effects since other installed packages # might not be required by Poetry but still taken into account when resolving dependencies. installed_repository = InstalledRepository.load(system_env, with_dependencies=True) root_package = None for package in installed_repository.packages: if package.name == "poetry": root_package = ProjectPackage(package.name, package.version) for dependency in package.requires: root_package.add_dependency(dependency) break root_package.python_versions = ".".join( str(v) for v in system_env.version_info[:3]) # We create a `pyproject.toml` file based on all the information # we have about the current environment. if not env_dir.joinpath("pyproject.toml").exists(): Factory.create_pyproject_from_package(root_package, env_dir) # We add the plugins to the dependencies section of the previously # created `pyproject.toml` file pyproject = PyProjectTOML(env_dir.joinpath("pyproject.toml")) poetry_content = pyproject.poetry_config poetry_dependency_section = poetry_content["dependencies"] plugin_names = [] for plugin in plugins: if "version" in plugin: # Validate version constraint parse_constraint(plugin["version"]) constraint = tomlkit.inline_table() for name, value in plugin.items(): if name == "name": continue constraint[name] = value if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] poetry_dependency_section[plugin["name"]] = constraint plugin_names.append(plugin["name"]) pyproject.save() # From this point forward, all the logic will be deferred to # the update command, by using the previously created `pyproject.toml` # file. application = cast(Application, self.application) update_command: UpdateCommand = cast(UpdateCommand, application.find("update")) # We won't go through the event dispatching done by the application # so we need to configure the command manually update_command.set_poetry(Factory().create_poetry(env_dir)) update_command.set_env(system_env) application._configure_installer(update_command, self._io) argv = ["update"] + plugin_names if self.option("dry-run"): argv.append("--dry-run") return update_command.run( IO( StringInput(" ".join(argv)), self._io.output, self._io.error_output, ))