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
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)
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 _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)