Exemple #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
        ]
Exemple #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
        ]
Exemple #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
        ]
Exemple #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 satisfier(self, term: Term) -> Assignment:
        """
        Returns the first Assignment in this solution such that the sublist of
        assignments up to and including that entry collectively satisfies term.
        """
        assigned_term = None

        for assignment in self._assignments:
            if assignment.dependency.complete_name != term.dependency.complete_name:
                continue

            if (not assignment.dependency.is_root
                    and not assignment.dependency.is_same_package_as(
                        term.dependency)):
                if not assignment.is_positive():
                    continue

                assert not term.is_positive()

                return assignment

            if assigned_term is None:
                assigned_term = assignment
            else:
                assigned_term = assigned_term.intersect(assignment)

            # As soon as we have enough assignments to satisfy term, return them.
            if assigned_term.satisfies(term):
                return assignment

        raise RuntimeError(f"[BUG] {term} is not satisfied.")
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
Exemple #7
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)
Exemple #8
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)
    def _register(self, assignment: Assignment) -> None:
        """
        Registers an Assignment in _positive or _negative.
        """
        name = assignment.dependency.complete_name
        old_positive = self._positive.get(name)
        if old_positive is None and assignment.dependency.features:
            old_positive_without_features = self._positive.get(
                assignment.dependency.name
            )
            if old_positive_without_features is not None:
                dep = old_positive_without_features.dependency.with_features(
                    assignment.dependency.features
                )
                old_positive = Term(dep, is_positive=True)
        if old_positive is not None:
            value = old_positive.intersect(assignment)
            assert value is not None
            self._positive[name] = value

            return

        ref = assignment.dependency.complete_name
        negative_by_ref = self._negative.get(name)
        old_negative = None if negative_by_ref is None else negative_by_ref.get(ref)
        term = (
            assignment if old_negative is None else assignment.intersect(old_negative)
        )
        assert term is not None

        if term.is_positive():
            if name in self._negative:
                del self._negative[name]

            self._positive[name] = term
        else:
            if name not in self._negative:
                self._negative[name] = {}

            self._negative[name][ref] = term
Exemple #10
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
        ]
Exemple #11
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
Exemple #12
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