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