Exemplo n.º 1
0
    def incompatibilities_for(
        self, package
    ):  # type: (DependencyPackage) -> List[Incompatibility]
        """
        Returns incompatibilities that encapsulate a given package's dependencies,
        or that it can't be safely selected.

        If multiple subsequent versions of this package have the same
        dependencies, this will return incompatibilities that reflect that. It
        won't return incompatibilities that have already been returned by a
        previous call to _incompatibilities_for().
        """
        if package.is_root():
            dependencies = package.all_requires
        else:
            dependencies = package.requires

            if not package.python_constraint.allows_all(
                self._package.python_constraint
            ):
                transitive_python_constraint = get_python_constraint_from_marker(
                    package.dependency.transitive_marker
                )
                intersection = package.python_constraint.intersect(
                    transitive_python_constraint
                )
                difference = transitive_python_constraint.difference(intersection)
                if (
                    transitive_python_constraint.is_any()
                    or self._package.python_constraint.intersect(
                        package.dependency.python_constraint
                    ).is_empty()
                    or intersection.is_empty()
                    or not difference.is_empty()
                ):
                    return [
                        Incompatibility(
                            [Term(package.to_dependency(), True)],
                            PythonCause(
                                package.python_versions, self._package.python_versions
                            ),
                        )
                    ]

        dependencies = [
            dep
            for dep in dependencies
            if dep.name not in self.UNSAFE_PACKAGES
            and self._package.python_constraint.allows_any(dep.python_constraint)
        ]

        return [
            Incompatibility(
                [Term(package.to_dependency(), True), Term(dep, False)],
                DependencyCause(),
            )
            for dep in dependencies
        ]
Exemplo n.º 2
0
    def incompatibilities_for(
            self, package: DependencyPackage) -> list[Incompatibility]:
        """
        Returns incompatibilities that encapsulate a given package's dependencies,
        or that it can't be safely selected.

        If multiple subsequent versions of this package have the same
        dependencies, this will return incompatibilities that reflect that. It
        won't return incompatibilities that have already been returned by a
        previous call to _incompatibilities_for().
        """
        if package.is_root():
            dependencies = package.all_requires
        else:
            dependencies = package.requires

            if not package.python_constraint.allows_all(
                    self._python_constraint):
                transitive_python_constraint = get_python_constraint_from_marker(
                    package.dependency.transitive_marker)
                intersection = package.python_constraint.intersect(
                    transitive_python_constraint)
                difference = transitive_python_constraint.difference(
                    intersection)

                # The difference is only relevant if it intersects
                # the root package python constraint
                difference = difference.intersect(self._python_constraint)
                if (transitive_python_constraint.is_any()
                        or self._python_constraint.intersect(
                            package.dependency.python_constraint).is_empty()
                        or intersection.is_empty()
                        or not difference.is_empty()):
                    return [
                        Incompatibility(
                            [Term(package.to_dependency(), True)],
                            PythonCause(package.python_versions,
                                        str(self._python_constraint)),
                        )
                    ]

        _dependencies = [
            dep for dep in dependencies if dep.name not in self.UNSAFE_PACKAGES
            and self._python_constraint.allows_any(dep.python_constraint) and (
                not self._env or dep.marker.validate(self._env.marker_env))
        ]
        dependencies = self._get_dependencies_with_overrides(
            _dependencies, package)

        return [
            Incompatibility(
                [Term(package.to_dependency(), True),
                 Term(dep, False)],
                DependencyCause(),
            ) for dep in dependencies
        ]
Exemplo n.º 3
0
    def incompatibilities_for(
            self, package):  # type: (Package) -> List[Incompatibility]
        """
        Returns incompatibilities that encapsulate a given package's dependencies,
        or that it can't be safely selected.

        If multiple subsequent versions of this package have the same
        dependencies, this will return incompatibilities that reflect that. It
        won't return incompatibilities that have already been returned by a
        previous call to _incompatibilities_for().
        """
        if package.is_root():
            dependencies = package.all_requires
        else:
            dependencies = package.requires

        if not self._package.python_constraint.allows_any(
                package.python_constraint):
            return [
                Incompatibility(
                    [Term(package.to_dependency(), True)],
                    PythonCause(package.python_versions,
                                self._package.python_versions),
                )
            ]

        if not self._package.platform_constraint.matches(
                package.platform_constraint):
            return [
                Incompatibility(
                    [Term(package.to_dependency(), True)],
                    PlatformCause(package.platform),
                )
            ]

        dependencies = [
            dep for dep in dependencies
            if dep.name not in self.UNSAFE_PACKAGES and self._package.
            python_constraint.allows_any(dep.python_constraint) and
            self._package.platform_constraint.matches(dep.platform_constraint)
        ]

        return [
            Incompatibility(
                [Term(package.to_dependency(), True),
                 Term(dep, False)],
                DependencyCause(),
            ) for dep in dependencies
        ]
Exemplo n.º 4
0
    def solve(self) -> SolverResult:
        """
        Finds a set of dependencies that match the root package's constraints,
        or raises an error if no such set is available.
        """
        start = time.time()
        root_dependency = Dependency(self._root.name, self._root.version)
        root_dependency.is_root = True

        self._add_incompatibility(
            Incompatibility([Term(root_dependency, False)], RootCause())
        )

        try:
            next: str | None = self._root.name
            while next is not None:
                self._propagate(next)
                next = self._choose_package_version()

            return self._result()
        except Exception:
            raise
        finally:
            self._log(
                f"Version solving took {time.time() - start:.3f} seconds.\n"
                f"Tried {self._solution.attempted_solutions} solutions."
            )
def test_it_provides_the_correct_solution():
    from poetry.mixology.solutions.solutions import PythonRequirementSolution

    incompatibility = Incompatibility(
        [Term(Dependency("foo", "^1.0"), True)], PythonCause("^3.5", ">=3.6")
    )
    exception = SolverProblemError(SolveFailure(incompatibility))
    solution = PythonRequirementSolution(exception)

    title = "Check your dependencies Python requirement."
    description = """\
The Python requirement can be specified via the `python` or `markers` properties

For foo, a possible solution would be to set the `python` property to ">=3.6,<4.0"\
"""
    links = [
        "https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
        "https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
    ]

    assert title == solution.solution_title
    assert (
        description == BufferedIO().remove_format(solution.solution_description).strip()
    )
    assert links == solution.documentation_links
Exemplo n.º 6
0
def test_it_cannot_solve_other_solver_errors():
    from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider

    incompatibility = Incompatibility([Term(Dependency("foo", "^1.0"), True)],
                                      NoVersionsCause())
    exception = SolverProblemError(SolveFailure(incompatibility))
    provider = PythonRequirementSolutionProvider()

    assert not provider.can_solve(exception)
Exemplo n.º 7
0
def test_it_can_solve_python_incompatibility_solver_errors():
    from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider
    from poetry.mixology.solutions.solutions import PythonRequirementSolution

    incompatibility = Incompatibility([Term(Dependency("foo", "^1.0"), True)],
                                      PythonCause("^3.5", ">=3.6"))
    exception = SolverProblemError(SolveFailure(incompatibility))
    provider = PythonRequirementSolutionProvider()

    assert provider.can_solve(exception)
    assert isinstance(
        provider.get_solutions(exception)[0], PythonRequirementSolution)
Exemplo n.º 8
0
    def incompatibilities_for(
            self, package):  # type: (Package) -> List[Incompatibility]
        """
        Returns incompatibilities that encapsulate a given package's dependencies,
        or that it can't be safely selected.

        If multiple subsequent versions of this package have the same
        dependencies, this will return incompatibilities that reflect that. It
        won't return incompatibilities that have already been returned by a
        previous call to _incompatibilities_for().
        """
        if package.source_type in ['git', 'file', 'directory']:
            dependencies = package.requires
        elif package.is_root():
            dependencies = package.all_requires
        else:
            dependencies = self._dependencies_for(package)

        if not self._package.python_constraint.allows_any(
                package.python_constraint):
            return [
                Incompatibility([Term(package.to_dependency(), True)],
                                PythonCause(package.python_versions))
            ]

        if not self._package.platform_constraint.matches(
                package.platform_constraint):
            return [
                Incompatibility([Term(package.to_dependency(), True)],
                                PlatformCause(package.platform))
            ]

        return [
            Incompatibility(
                [Term(package.to_dependency(), True),
                 Term(dep, False)], DependencyCause()) for dep in dependencies
        ]
Exemplo n.º 9
0
    def _choose_package_version(self) -> str | None:
        """
        Tries to select a version of a required package.

        Returns the name of the package whose incompatibilities should be
        propagated by _propagate(), or None indicating that version solving is
        complete and a solution has been found.
        """
        unsatisfied = self._solution.unsatisfied
        if not unsatisfied:
            return None

        # Prefer packages with as few remaining versions as possible,
        # so that if a conflict is necessary it's forced quickly.
        def _get_min(dependency: Dependency) -> tuple[bool, int]:
            if dependency.name in self._use_latest:
                # If we're forced to use the latest version of a package, it effectively
                # only has one version to choose from.
                return not dependency.marker.is_any(), 1

            locked = self._get_locked(dependency)
            if locked:
                return not dependency.marker.is_any(), 1

            # VCS, URL, File or Directory dependencies
            # represent a single version
            if (
                dependency.is_vcs()
                or dependency.is_url()
                or dependency.is_file()
                or dependency.is_directory()
            ):
                return not dependency.marker.is_any(), 1

            try:
                return (
                    not dependency.marker.is_any(),
                    len(self._dependency_cache.search_for(dependency)),
                )
            except ValueError:
                return not dependency.marker.is_any(), 0

        if len(unsatisfied) == 1:
            dependency = unsatisfied[0]
        else:
            dependency = min(*unsatisfied, key=_get_min)

        locked = self._get_locked(dependency)
        if locked is None:
            try:
                packages = self._dependency_cache.search_for(dependency)
            except ValueError as e:
                self._add_incompatibility(
                    Incompatibility([Term(dependency, True)], PackageNotFoundCause(e))
                )
                complete_name: str = dependency.complete_name
                return complete_name

            package = None
            if dependency.name not in self._use_latest:
                # prefer locked version of compatible (not exact same) dependency;
                # required in order to not unnecessarily update dependencies with
                # extras, e.g. "coverage" vs. "coverage[toml]"
                locked = self._get_locked(dependency, allow_similar=True)
            if locked is not None:
                package = next(
                    (p for p in packages if p.version == locked.version), None
                )
            if package is None:
                with suppress(IndexError):
                    package = packages[0]

            if package is None:
                # If there are no versions that satisfy the constraint,
                # add an incompatibility that indicates that.
                self._add_incompatibility(
                    Incompatibility([Term(dependency, True)], NoVersionsCause())
                )

                complete_name = dependency.complete_name
                return complete_name
        else:
            package = locked

        package = self._provider.complete_package(package)

        conflict = False
        for incompatibility in self._provider.incompatibilities_for(package):
            self._add_incompatibility(incompatibility)

            # If an incompatibility is already satisfied, then selecting version
            # would cause a conflict.
            #
            # We'll continue adding its dependencies, then go back to
            # unit propagation which will guide us to choose a better version.
            conflict = conflict or all(
                term.dependency.complete_name == dependency.complete_name
                or self._solution.satisfies(term)
                for term in incompatibility.terms
            )

        if not conflict:
            self._solution.decide(package.package)
            self._log(
                f"selecting {package.complete_name} ({package.full_pretty_version})"
            )

        complete_name = dependency.complete_name
        return complete_name
Exemplo n.º 10
0
    def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility:
        """
        Given an incompatibility that's satisfied by _solution,
        The `conflict resolution`_ constructs a new incompatibility that encapsulates
        the root cause of the conflict and backtracks _solution until the new
        incompatibility will allow _propagate() to deduce new assignments.

        Adds the new incompatibility to _incompatibilities and returns it.

        .. _conflict resolution:
        https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution
        """
        self._log(f"conflict: {incompatibility}")

        new_incompatibility = False
        while not incompatibility.is_failure():
            # The term in incompatibility.terms that was most recently satisfied by
            # _solution.
            most_recent_term = None

            # The earliest assignment in _solution such that incompatibility is
            # satisfied by _solution up to and including this assignment.
            most_recent_satisfier = None

            # The difference between most_recent_satisfier and most_recent_term;
            # that is, the versions that are allowed by most_recent_satisfier and not
            # by most_recent_term. This is None if most_recent_satisfier totally
            # satisfies most_recent_term.
            difference = None

            # The decision level of the earliest assignment in _solution *before*
            # most_recent_satisfier such that incompatibility is satisfied by
            # _solution up to and including this assignment plus
            # most_recent_satisfier.
            #
            # Decision level 1 is the level where the root package was selected. It's
            # safe to go back to decision level 0, but stopping at 1 tends to produce
            # better error messages, because references to the root package end up
            # closer to the final conclusion that no solution exists.
            previous_satisfier_level = 1

            for term in incompatibility.terms:
                satisfier = self._solution.satisfier(term)

                if most_recent_satisfier is None:
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                elif most_recent_satisfier.index < satisfier.index:
                    previous_satisfier_level = max(
                        previous_satisfier_level, most_recent_satisfier.decision_level
                    )
                    most_recent_term = term
                    most_recent_satisfier = satisfier
                    difference = None
                else:
                    previous_satisfier_level = max(
                        previous_satisfier_level, satisfier.decision_level
                    )

                if most_recent_term == term:
                    # If most_recent_satisfier doesn't satisfy most_recent_term on its
                    # own, then the next-most-recent satisfier may be the one that
                    # satisfies the remainder.
                    difference = most_recent_satisfier.difference(most_recent_term)
                    if difference is not None:
                        previous_satisfier_level = max(
                            previous_satisfier_level,
                            self._solution.satisfier(difference.inverse).decision_level,
                        )

            # If most_recent_identifier is the only satisfier left at its decision
            # level, or if it has no cause (indicating that it's a decision rather
            # than a derivation), then incompatibility is the root cause. We then
            # backjump to previous_satisfier_level, where incompatibility is
            # guaranteed to allow _propagate to produce more assignments.

            # using assert to suppress mypy [union-attr]
            assert most_recent_satisfier is not None
            if (
                previous_satisfier_level < most_recent_satisfier.decision_level
                or most_recent_satisfier.cause is None
            ):
                self._solution.backtrack(previous_satisfier_level)
                self._contradicted_incompatibilities.clear()
                self._dependency_cache.clear()
                if new_incompatibility:
                    self._add_incompatibility(incompatibility)

                return incompatibility

            # Create a new incompatibility by combining incompatibility with the
            # incompatibility that caused most_recent_satisfier to be assigned. Doing
            # this iteratively constructs an incompatibility that's guaranteed to be
            # true (that is, we know for sure no solution will satisfy the
            # incompatibility) while also approximating the intuitive notion of the
            # "root cause" of the conflict.
            new_terms = [
                term for term in incompatibility.terms if term != most_recent_term
            ]

            for term in most_recent_satisfier.cause.terms:
                if term.dependency != most_recent_satisfier.dependency:
                    new_terms.append(term)

            # The most_recent_satisfier may not satisfy most_recent_term on its own
            # if there are a collection of constraints on most_recent_term that
            # only satisfy it together. For example, if most_recent_term is
            # `foo ^1.0.0` and _solution contains `[foo >=1.0.0,
            # foo <2.0.0]`, then most_recent_satisfier will be `foo <2.0.0` even
            # though it doesn't totally satisfy `foo ^1.0.0`.
            #
            # In this case, we add `not (most_recent_satisfier \ most_recent_term)` to
            # the incompatibility as well, See the `algorithm documentation`_ for
            # details.
            #
            # .. _algorithm documentation:
            # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution  # noqa: E501
            if difference is not None:
                new_terms.append(difference.inverse)

            incompatibility = Incompatibility(
                new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
            )
            new_incompatibility = True

            partially = "" if difference is None else " partially"
            self._log(
                f"! {most_recent_term} is{partially} satisfied by"
                f" {most_recent_satisfier}"
            )
            self._log(f'! which is caused by "{most_recent_satisfier.cause}"')
            self._log(f"! thus: {incompatibility}")

        raise SolveFailure(incompatibility)
Exemplo n.º 11
0
    def _choose_package_version(self) -> Optional[str]:
        """
        Tries to select a version of a required package.

        Returns the name of the package whose incompatibilities should be
        propagated by _propagate(), or None indicating that version solving is
        complete and a solution has been found.
        """
        unsatisfied = self._solution.unsatisfied
        if not unsatisfied:
            return None

        # Prefer packages with as few remaining versions as possible,
        # so that if a conflict is necessary it's forced quickly.
        def _get_min(dependency: Dependency) -> Tuple[bool, int]:
            if dependency.name in self._use_latest:
                # If we're forced to use the latest version of a package, it effectively
                # only has one version to choose from.
                return not dependency.marker.is_any(), 1

            locked = self._get_locked(dependency)
            if locked and (dependency.constraint.allows(locked.version)
                           or locked.is_prerelease()
                           and dependency.constraint.allows(
                               locked.version.next_patch())):
                return not dependency.marker.is_any(), 1

            # VCS, URL, File or Directory dependencies
            # represent a single version
            if (dependency.is_vcs() or dependency.is_url()
                    or dependency.is_file() or dependency.is_directory()):
                return not dependency.marker.is_any(), 1

            try:
                return (
                    not dependency.marker.is_any(),
                    len(self._provider.search_for(dependency)),
                )
            except ValueError:
                return not dependency.marker.is_any(), 0

        if len(unsatisfied) == 1:
            dependency = unsatisfied[0]
        else:
            dependency = min(*unsatisfied, key=_get_min)

        locked = self._get_locked(dependency)
        if locked is None or not dependency.constraint.allows(locked.version):
            try:
                packages = self._provider.search_for(dependency)
            except ValueError as e:
                self._add_incompatibility(
                    Incompatibility([Term(dependency, True)],
                                    PackageNotFoundCause(e)))
                return dependency.complete_name

            try:
                version = packages[0]
            except IndexError:
                version = None

            if version is None:
                # If there are no versions that satisfy the constraint,
                # add an incompatibility that indicates that.
                self._add_incompatibility(
                    Incompatibility([Term(dependency, True)],
                                    NoVersionsCause()))

                return dependency.complete_name
        else:
            version = locked

        version = self._provider.complete_package(version)

        conflict = False
        for incompatibility in self._provider.incompatibilities_for(version):
            self._add_incompatibility(incompatibility)

            # If an incompatibility is already satisfied, then selecting version
            # would cause a conflict.
            #
            # We'll continue adding its dependencies, then go back to
            # unit propagation which will guide us to choose a better version.
            conflict = conflict or all(
                term.dependency.complete_name == dependency.complete_name
                or self._solution.satisfies(term)
                for term in incompatibility.terms)

        if not conflict:
            self._solution.decide(version)
            self._log(
                f"selecting {version.complete_name} ({version.full_pretty_version})"
            )

        return dependency.complete_name