From 799217114a556742a10ce67f5c94f05c9003c580 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 2 Aug 2021 16:19:43 +0300 Subject: [PATCH 01/11] add message when backtracking starts --- .../resolution/resolvelib/factory.py | 200 ++++++++++-------- .../resolution/resolvelib/reporter.py | 9 +- .../resolution/resolvelib/resolver.py | 3 +- tests/functional/test_new_resolver.py | 108 ++++++---- 4 files changed, 190 insertions(+), 130 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c3aaa6957f5..53dc776f3dc 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -69,11 +69,11 @@ if TYPE_CHECKING: from typing import Protocol + class ConflictCause(Protocol): requirement: RequiresPythonRequirement parent: Candidate - logger = logging.getLogger(__name__) C = TypeVar("C") @@ -88,16 +88,16 @@ class CollectedRootRequirements(NamedTuple): class Factory: def __init__( - self, - finder: PackageFinder, - preparer: RequirementPreparer, - make_install_req: InstallRequirementProvider, - wheel_cache: Optional[WheelCache], - use_user_site: bool, - force_reinstall: bool, - ignore_installed: bool, - ignore_requires_python: bool, - py_version_info: Optional[Tuple[int, ...]] = None, + self, + finder: PackageFinder, + preparer: RequirementPreparer, + make_install_req: InstallRequirementProvider, + wheel_cache: Optional[WheelCache], + use_user_site: bool, + force_reinstall: bool, + ignore_installed: bool, + ignore_requires_python: bool, + py_version_info: Optional[Tuple[int, ...]] = None, ) -> None: self._finder = finder self.preparer = preparer @@ -139,7 +139,7 @@ def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None: raise UnsupportedWheel(msg) def _make_extras_candidate( - self, base: BaseCandidate, extras: FrozenSet[str] + self, base: BaseCandidate, extras: FrozenSet[str] ) -> ExtrasCandidate: cache_key = (id(base), extras) try: @@ -150,10 +150,10 @@ def _make_extras_candidate( return candidate def _make_candidate_from_dist( - self, - dist: BaseDistribution, - extras: FrozenSet[str], - template: InstallRequirement, + self, + dist: BaseDistribution, + extras: FrozenSet[str], + template: InstallRequirement, ) -> Candidate: try: base = self._installed_candidate_cache[dist.canonical_name] @@ -165,12 +165,12 @@ def _make_candidate_from_dist( return self._make_extras_candidate(base, extras) def _make_candidate_from_link( - self, - link: Link, - extras: FrozenSet[str], - template: InstallRequirement, - name: Optional[NormalizedName], - version: Optional[CandidateVersion], + self, + link: Link, + extras: FrozenSet[str], + template: InstallRequirement, + name: Optional[NormalizedName], + version: Optional[CandidateVersion], ) -> Optional[Candidate]: # TODO: Check already installed candidate, and use it if the link and # editable flag match. @@ -216,12 +216,12 @@ def _make_candidate_from_link( return self._make_extras_candidate(base, extras) def _iter_found_candidates( - self, - ireqs: Sequence[InstallRequirement], - specifier: SpecifierSet, - hashes: Hashes, - prefers_installed: bool, - incompatible_ids: Set[int], + self, + ireqs: Sequence[InstallRequirement], + specifier: SpecifierSet, + hashes: Hashes, + prefers_installed: bool, + incompatible_ids: Set[int], ) -> Iterable[Candidate]: if not ireqs: return () @@ -300,9 +300,9 @@ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]: ) def _iter_explicit_candidates_from_base( - self, - base_requirements: Iterable[Requirement], - extras: FrozenSet[str], + self, + base_requirements: Iterable[Requirement], + extras: FrozenSet[str], ) -> Iterator[Candidate]: """Produce explicit candidates from the base given an extra-ed package. @@ -322,10 +322,10 @@ def _iter_explicit_candidates_from_base( yield self._make_extras_candidate(base_cand, extras) def _iter_candidates_from_constraints( - self, - identifier: str, - constraint: Constraint, - template: InstallRequirement, + self, + identifier: str, + constraint: Constraint, + template: InstallRequirement, ) -> Iterator[Candidate]: """Produce explicit candidates from constraints. @@ -410,12 +410,12 @@ def find_candidates( c for c in explicit_candidates if id(c) not in incompat_ids - and constraint.is_satisfied_by(c) - and all(req.is_satisfied_by(c) for req in requirements[identifier]) + and constraint.is_satisfied_by(c) + and all(req.is_satisfied_by(c) for req in requirements[identifier]) ) def _make_requirement_from_install_req( - self, ireq: InstallRequirement, requested_extras: Iterable[str] + self, ireq: InstallRequirement, requested_extras: Iterable[str] ) -> Optional[Requirement]: if not ireq.match_markers(requested_extras): logger.info( @@ -447,7 +447,7 @@ def _make_requirement_from_install_req( return self.make_requirement_from_candidate(cand) def collect_root_requirements( - self, root_ireqs: List[InstallRequirement] + self, root_ireqs: List[InstallRequirement] ) -> CollectedRootRequirements: collected = CollectedRootRequirements([], {}, {}) for i, ireq in enumerate(root_ireqs): @@ -477,7 +477,7 @@ def collect_root_requirements( return collected def make_requirement_from_candidate( - self, candidate: Candidate + self, candidate: Candidate ) -> ExplicitRequirement: return ExplicitRequirement(candidate) @@ -502,7 +502,7 @@ def make_requires_python_requirement( return RequiresPythonRequirement(specifier, self._python_candidate) def get_wheel_cache_entry( - self, link: Link, name: Optional[str] + self, link: Link, name: Optional[str] ) -> Optional[CacheEntry]: """Look up the link in the wheel cache. @@ -549,7 +549,7 @@ def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistributi return None def _report_requires_python_error( - self, causes: Sequence["ConflictCause"] + self, causes: Sequence["ConflictCause"] ) -> UnsupportedPythonVersion: assert causes, "Requires-Python error reported with no cause" @@ -571,7 +571,7 @@ def _report_requires_python_error( return UnsupportedPythonVersion(message) def _report_single_requirement_conflict( - self, req: Requirement, parent: Optional[Candidate] + self, req: Requirement, parent: Optional[Candidate] ) -> DistributionNotFound: if parent is None: req_disp = str(req) @@ -598,21 +598,17 @@ def _report_single_requirement_conflict( return DistributionNotFound(f"No matching distribution found for {req}") def get_installation_error( - self, - e: "ResolutionImpossible[Requirement, Candidate]", - constraints: Dict[str, Constraint], + self, + e: "ResolutionImpossible[Requirement, Candidate]", + constraints: Dict[str, Constraint], ) -> InstallationError: - assert e.causes, "Installation error reported with no cause" + failure_causes = e.causes + assert failure_causes, "Installation error reported with no cause" # If one of the things we can't solve is "we need Python X.Y", # that is what we report. - requires_python_causes = [ - cause - for cause in e.causes - if isinstance(cause.requirement, RequiresPythonRequirement) - and not cause.requirement.is_satisfied_by(self._python_candidate) - ] + requires_python_causes = self.extract_requires_python_causes(failure_causes) if requires_python_causes: # The comprehension above makes sure all Requirement instances are # RequiresPythonRequirement, so let's cast for convenience. @@ -625,14 +621,65 @@ def get_installation_error( # The simplest case is when we have *one* cause that can't be # satisfied. We just report that case. - if len(e.causes) == 1: - req, parent = e.causes[0] + if len(failure_causes) == 1: + req, parent = failure_causes[0] if req.name not in constraints: return self._report_single_requirement_conflict(req, parent) # OK, we now have a list of requirements that can't all be # satisfied at once. + logger.critical(self.triggers_message(failure_causes)) + + msg = ( + self.causes_message(constraints, failure_causes) + + "\n\n" + + "To fix this you could try to:\n" + + "1. loosen the range of package versions you've specified\n" + + "2. remove package versions to allow pip attempt to solve " + + "the dependency conflict\n" + ) + + logger.info(msg) + + return DistributionNotFound( + "ResolutionImpossible: for help visit " + "https://pip.pypa.io/en/latest/topics/dependency-resolution/" + "#dealing-with-dependency-conflicts" + ) + + def get_backtracking_reason_message(self, backtracking_causes, constraints): + requires_python_causes = self.extract_requires_python_causes(backtracking_causes) + if requires_python_causes or len(backtracking_causes) == 1: + # no message when python causes or a single failure, since this is probably a genuine problem + return + + # OK, we now have a list of requirements that can't all be + # satisfied at once. + + return (self.triggers_message(backtracking_causes) + + self.causes_message(constraints, backtracking_causes)) + + @staticmethod + def causes_message(constraints, failure_causes): + msg = "\nThe conflict is caused by:" + relevant_constraints = set() + for req, parent in failure_causes: + if req.name in constraints: + relevant_constraints.add(req.name) + msg = msg + "\n " + if parent: + msg = msg + f"{parent.name} {parent.version} depends on " + else: + msg = msg + "The user requested " + msg = msg + req.format_for_error() + for key in relevant_constraints: + spec = constraints[key].specifier + msg += f"\n The user requested (constraint) {key}{spec}" + return msg + + @staticmethod + def triggers_message(failure_causes): # A couple of formatting helpers def text_join(parts: List[str]) -> str: if len(parts) == 1: @@ -649,53 +696,28 @@ def describe_trigger(parent: Candidate) -> str: return str(ireq.comes_from) triggers = set() - for req, parent in e.causes: + for req, parent in failure_causes: if parent is None: # This is a root requirement, so we can report it directly trigger = req.format_for_error() else: trigger = describe_trigger(parent) triggers.add(trigger) - if triggers: info = text_join(sorted(triggers)) else: info = "the requested packages" - msg = ( "Cannot install {} because these package versions " "have conflicting dependencies.".format(info) ) - logger.critical(msg) - msg = "\nThe conflict is caused by:" - - relevant_constraints = set() - for req, parent in e.causes: - if req.name in constraints: - relevant_constraints.add(req.name) - msg = msg + "\n " - if parent: - msg = msg + f"{parent.name} {parent.version} depends on " - else: - msg = msg + "The user requested " - msg = msg + req.format_for_error() - for key in relevant_constraints: - spec = constraints[key].specifier - msg += f"\n The user requested (constraint) {key}{spec}" - - msg = ( - msg - + "\n\n" - + "To fix this you could try to:\n" - + "1. loosen the range of package versions you've specified\n" - + "2. remove package versions to allow pip attempt to solve " - + "the dependency conflict\n" - ) + return msg - logger.info(msg) - return DistributionNotFound( - "ResolutionImpossible: for help visit " - "https://pip.pypa.io/en/latest/topics/dependency-resolution/" - "#dealing-with-dependency-conflicts" - ) + def extract_requires_python_causes(self, failure_causes): + return [ + cause + for cause in failure_causes + if isinstance(cause.requirement, RequiresPythonRequirement) + and not cause.requirement.is_satisfied_by(self._python_candidate) + ] diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 6ced5329b81..f8195bee47d 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -10,8 +10,9 @@ class PipReporter(BaseReporter): - def __init__(self) -> None: + def __init__(self, backtracking_message_generator) -> None: self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) + self.backtracking_message_generator = backtracking_message_generator self._messages_at_backtrack = { 1: ( @@ -42,6 +43,9 @@ def backtracking(self, candidate: Candidate) -> None: message = self._messages_at_backtrack[count] logger.info("INFO: %s", message.format(package_name=candidate.name)) + def start_backtracking(self, causes: Any) -> None: + logger.info("INFO: %s", self.backtracking_message_generator(causes)) + class PipDebuggingReporter(BaseReporter): """A reporter that does an info log for every event it sees.""" @@ -64,5 +68,8 @@ def adding_requirement(self, requirement: Requirement, parent: Candidate) -> Non def backtracking(self, candidate: Candidate) -> None: logger.info("Reporter.backtracking(%r)", candidate) + def start_backtracking(self, cause: Any) -> None: + logger.info("Reporter.start_backtracking(%r)", cause) + def pinning(self, candidate: Candidate) -> None: logger.info("Reporter.pinning(%r)", candidate) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 12f96702024..4d36396f5cc 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -81,7 +81,8 @@ def resolve( if "PIP_RESOLVER_DEBUG" in os.environ: reporter: BaseReporter = PipDebuggingReporter() else: - reporter = PipReporter() + reporter = PipReporter( + functools.partial(self.factory.get_backtracking_reason_message, constraints=collected.constraints)) resolver: RLResolver[Requirement, Candidate, str] = RLResolver( provider, reporter, diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index abff7d4f056..934f345f96c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -342,10 +342,10 @@ def test_new_resolver_installs_editable(script): ], ) def test_new_resolver_requires_python( - script, - requires_python, - ignore_requires_python, - dep_version, + script, + requires_python, + ignore_requires_python, + dep_version, ): create_basic_wheel_for_package( script, @@ -592,10 +592,10 @@ def test_new_resolver_force_reinstall(script): ids=["default", "exact-pre", "explicit-pre", "no-stable"], ) def test_new_resolver_handles_prerelease( - script, - available_versions, - pip_args, - expected_version, + script, + available_versions, + pip_args, + expected_version, ): for version in available_versions: create_basic_wheel_for_package(script, "pkg", version) @@ -686,16 +686,16 @@ def test_new_resolver_constraint_no_specifier(script): "constraint, error", [ ( - "dist.zip", - "Unnamed requirements are not allowed as constraints", + "dist.zip", + "Unnamed requirements are not allowed as constraints", ), ( - "-e git+https://example.com/dist.git#egg=req", - "Editable requirements are not allowed as constraints", + "-e git+https://example.com/dist.git#egg=req", + "Editable requirements are not allowed as constraints", ), ( - "pkg[extra]", - "Constraints cannot have extras", + "pkg[extra]", + "Constraints cannot have extras", ), ], ) @@ -747,10 +747,10 @@ def test_new_resolver_constraint_on_dependency(script): ], ) def test_new_resolver_constraint_on_path_empty( - script, - constraint_version, - expect_error, - message, + script, + constraint_version, + expect_error, + message, ): """A path requirement can be filtered by a constraint.""" setup_py = script.scratch_path / "setup.py" @@ -1163,6 +1163,36 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): assert "press Ctrl + C" in result.stdout +def test_new_resolver_presents_messages_when_backtracking_starts(script): + packages = [ + ("a", "2.0.0", ["c==2.0.0"]), + ("a", "1.0.0", ["c==1.0.0"]), + ("b", "1.0.0", ["c==1.0.0"]), + ("c", "1.0.0", []), + ("c", "2.0.0", []), + ] + + for name, version, depends in packages: + create_basic_wheel_for_package(script, name, version, depends=depends) + + # Install A and B + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", script.scratch_path, + "a", "b" + ) + + assert_installed(script, A="1.0.0", B="1.0.0", C="1.0.0") + message = """INFO: Cannot install a==2.0.0 and b==1.0.0 because these package versions have conflicting dependencies. +The conflict is caused by: + a 2.0.0 depends on c==2.0.0 + b 1.0.0 depends on c==1.0.0""" + stdout = result.stdout + assert message in stdout + + @pytest.mark.parametrize( "metadata_version", [ @@ -1186,9 +1216,9 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): ids=["file_dot", "file_underscore"], ) def test_new_resolver_check_wheel_version_normalized( - script, - metadata_version, - filename_version, + script, + metadata_version, + filename_version, ): filename = f"simple-{filename_version}-py2.py3-none-any.whl" @@ -1575,8 +1605,8 @@ def test_new_resolver_fails_with_url_constraint_and_incompatible_version( assert "Cannot install test_pkg" in result.stderr, str(result) assert ( - "because these package versions have conflicting dependencies." - ) in result.stderr, str(result) + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1666,9 +1696,9 @@ def test_new_resolver_fails_on_needed_conflicting_constraints(script): ) assert ( - "Cannot install test_pkg because these package versions have conflicting " - "dependencies." - ) in result.stderr, str(result) + "Cannot install test_pkg because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1712,8 +1742,8 @@ def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script): assert "Cannot install test-pkg 0.2.0" in result.stderr, str(result) assert ( - "because these package versions have conflicting dependencies." - ) in result.stderr, str(result) + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1801,7 +1831,7 @@ def test_new_resolver_applies_url_constraint_to_dep(script): def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1840,7 +1870,7 @@ def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url( def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1865,15 +1895,15 @@ def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url( ) assert ( - "Cannot install base because these package versions have conflicting " - "dependencies." - ) in result.stderr, str(result) + "Cannot install base because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) script.assert_not_installed("base") def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("dep", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1968,11 +1998,11 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( ], ) def test_new_resolver_direct_url_equivalent( - tmp_path, - script, - suffixes_equivalent, - depend_suffix, - request_suffix, + tmp_path, + script, + suffixes_equivalent, + depend_suffix, + request_suffix, ): pkga = create_basic_wheel_for_package(script, name="pkga", version="1") pkgb = create_basic_wheel_for_package( From 31050bd3af6f38bb25cdfe7f598ca38b6626d8a6 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 2 Aug 2021 16:24:26 +0300 Subject: [PATCH 02/11] linting fixes --- .../resolution/resolvelib/factory.py | 115 +++++++++--------- .../resolution/resolvelib/resolver.py | 6 +- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 53dc776f3dc..ce6ea2147da 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -69,11 +69,11 @@ if TYPE_CHECKING: from typing import Protocol - class ConflictCause(Protocol): requirement: RequiresPythonRequirement parent: Candidate + logger = logging.getLogger(__name__) C = TypeVar("C") @@ -88,16 +88,16 @@ class CollectedRootRequirements(NamedTuple): class Factory: def __init__( - self, - finder: PackageFinder, - preparer: RequirementPreparer, - make_install_req: InstallRequirementProvider, - wheel_cache: Optional[WheelCache], - use_user_site: bool, - force_reinstall: bool, - ignore_installed: bool, - ignore_requires_python: bool, - py_version_info: Optional[Tuple[int, ...]] = None, + self, + finder: PackageFinder, + preparer: RequirementPreparer, + make_install_req: InstallRequirementProvider, + wheel_cache: Optional[WheelCache], + use_user_site: bool, + force_reinstall: bool, + ignore_installed: bool, + ignore_requires_python: bool, + py_version_info: Optional[Tuple[int, ...]] = None, ) -> None: self._finder = finder self.preparer = preparer @@ -139,7 +139,7 @@ def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None: raise UnsupportedWheel(msg) def _make_extras_candidate( - self, base: BaseCandidate, extras: FrozenSet[str] + self, base: BaseCandidate, extras: FrozenSet[str] ) -> ExtrasCandidate: cache_key = (id(base), extras) try: @@ -150,10 +150,10 @@ def _make_extras_candidate( return candidate def _make_candidate_from_dist( - self, - dist: BaseDistribution, - extras: FrozenSet[str], - template: InstallRequirement, + self, + dist: BaseDistribution, + extras: FrozenSet[str], + template: InstallRequirement, ) -> Candidate: try: base = self._installed_candidate_cache[dist.canonical_name] @@ -165,12 +165,12 @@ def _make_candidate_from_dist( return self._make_extras_candidate(base, extras) def _make_candidate_from_link( - self, - link: Link, - extras: FrozenSet[str], - template: InstallRequirement, - name: Optional[NormalizedName], - version: Optional[CandidateVersion], + self, + link: Link, + extras: FrozenSet[str], + template: InstallRequirement, + name: Optional[NormalizedName], + version: Optional[CandidateVersion], ) -> Optional[Candidate]: # TODO: Check already installed candidate, and use it if the link and # editable flag match. @@ -216,12 +216,12 @@ def _make_candidate_from_link( return self._make_extras_candidate(base, extras) def _iter_found_candidates( - self, - ireqs: Sequence[InstallRequirement], - specifier: SpecifierSet, - hashes: Hashes, - prefers_installed: bool, - incompatible_ids: Set[int], + self, + ireqs: Sequence[InstallRequirement], + specifier: SpecifierSet, + hashes: Hashes, + prefers_installed: bool, + incompatible_ids: Set[int], ) -> Iterable[Candidate]: if not ireqs: return () @@ -300,9 +300,9 @@ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]: ) def _iter_explicit_candidates_from_base( - self, - base_requirements: Iterable[Requirement], - extras: FrozenSet[str], + self, + base_requirements: Iterable[Requirement], + extras: FrozenSet[str], ) -> Iterator[Candidate]: """Produce explicit candidates from the base given an extra-ed package. @@ -322,10 +322,10 @@ def _iter_explicit_candidates_from_base( yield self._make_extras_candidate(base_cand, extras) def _iter_candidates_from_constraints( - self, - identifier: str, - constraint: Constraint, - template: InstallRequirement, + self, + identifier: str, + constraint: Constraint, + template: InstallRequirement, ) -> Iterator[Candidate]: """Produce explicit candidates from constraints. @@ -410,12 +410,12 @@ def find_candidates( c for c in explicit_candidates if id(c) not in incompat_ids - and constraint.is_satisfied_by(c) - and all(req.is_satisfied_by(c) for req in requirements[identifier]) + and constraint.is_satisfied_by(c) + and all(req.is_satisfied_by(c) for req in requirements[identifier]) ) def _make_requirement_from_install_req( - self, ireq: InstallRequirement, requested_extras: Iterable[str] + self, ireq: InstallRequirement, requested_extras: Iterable[str] ) -> Optional[Requirement]: if not ireq.match_markers(requested_extras): logger.info( @@ -447,7 +447,7 @@ def _make_requirement_from_install_req( return self.make_requirement_from_candidate(cand) def collect_root_requirements( - self, root_ireqs: List[InstallRequirement] + self, root_ireqs: List[InstallRequirement] ) -> CollectedRootRequirements: collected = CollectedRootRequirements([], {}, {}) for i, ireq in enumerate(root_ireqs): @@ -477,7 +477,7 @@ def collect_root_requirements( return collected def make_requirement_from_candidate( - self, candidate: Candidate + self, candidate: Candidate ) -> ExplicitRequirement: return ExplicitRequirement(candidate) @@ -502,7 +502,7 @@ def make_requires_python_requirement( return RequiresPythonRequirement(specifier, self._python_candidate) def get_wheel_cache_entry( - self, link: Link, name: Optional[str] + self, link: Link, name: Optional[str] ) -> Optional[CacheEntry]: """Look up the link in the wheel cache. @@ -549,7 +549,7 @@ def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistributi return None def _report_requires_python_error( - self, causes: Sequence["ConflictCause"] + self, causes: Sequence["ConflictCause"] ) -> UnsupportedPythonVersion: assert causes, "Requires-Python error reported with no cause" @@ -571,7 +571,7 @@ def _report_requires_python_error( return UnsupportedPythonVersion(message) def _report_single_requirement_conflict( - self, req: Requirement, parent: Optional[Candidate] + self, req: Requirement, parent: Optional[Candidate] ) -> DistributionNotFound: if parent is None: req_disp = str(req) @@ -598,9 +598,9 @@ def _report_single_requirement_conflict( return DistributionNotFound(f"No matching distribution found for {req}") def get_installation_error( - self, - e: "ResolutionImpossible[Requirement, Candidate]", - constraints: Dict[str, Constraint], + self, + e: "ResolutionImpossible[Requirement, Candidate]", + constraints: Dict[str, Constraint], ) -> InstallationError: failure_causes = e.causes @@ -632,12 +632,12 @@ def get_installation_error( logger.critical(self.triggers_message(failure_causes)) msg = ( - self.causes_message(constraints, failure_causes) - + "\n\n" - + "To fix this you could try to:\n" - + "1. loosen the range of package versions you've specified\n" - + "2. remove package versions to allow pip attempt to solve " - + "the dependency conflict\n" + self.causes_message(constraints, failure_causes) + + "\n\n" + + "To fix this you could try to:\n" + + "1. loosen the range of package versions you've specified\n" + + "2. remove package versions to allow pip attempt to solve " + + "the dependency conflict\n" ) logger.info(msg) @@ -649,7 +649,9 @@ def get_installation_error( ) def get_backtracking_reason_message(self, backtracking_causes, constraints): - requires_python_causes = self.extract_requires_python_causes(backtracking_causes) + requires_python_causes = self.extract_requires_python_causes( + backtracking_causes + ) if requires_python_causes or len(backtracking_causes) == 1: # no message when python causes or a single failure, since this is probably a genuine problem return @@ -657,8 +659,9 @@ def get_backtracking_reason_message(self, backtracking_causes, constraints): # OK, we now have a list of requirements that can't all be # satisfied at once. - return (self.triggers_message(backtracking_causes) + - self.causes_message(constraints, backtracking_causes)) + return self.triggers_message(backtracking_causes) + self.causes_message( + constraints, backtracking_causes + ) @staticmethod def causes_message(constraints, failure_causes): @@ -719,5 +722,5 @@ def extract_requires_python_causes(self, failure_causes): cause for cause in failure_causes if isinstance(cause.requirement, RequiresPythonRequirement) - and not cause.requirement.is_satisfied_by(self._python_candidate) + and not cause.requirement.is_satisfied_by(self._python_candidate) ] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 4d36396f5cc..8cc398f916e 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -82,7 +82,11 @@ def resolve( reporter: BaseReporter = PipDebuggingReporter() else: reporter = PipReporter( - functools.partial(self.factory.get_backtracking_reason_message, constraints=collected.constraints)) + functools.partial( + self.factory.get_backtracking_reason_message, + constraints=collected.constraints, + ) + ) resolver: RLResolver[Requirement, Candidate, str] = RLResolver( provider, reporter, From eac7c9c3840467d6d06e6c395229ed29992cf70f Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 2 Aug 2021 16:30:20 +0300 Subject: [PATCH 03/11] fixing liniting --- .../resolution/resolvelib/factory.py | 30 +++---- tests/functional/test_new_resolver.py | 85 ++++++++++--------- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index ce6ea2147da..0f1199f6005 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -648,21 +648,6 @@ def get_installation_error( "#dealing-with-dependency-conflicts" ) - def get_backtracking_reason_message(self, backtracking_causes, constraints): - requires_python_causes = self.extract_requires_python_causes( - backtracking_causes - ) - if requires_python_causes or len(backtracking_causes) == 1: - # no message when python causes or a single failure, since this is probably a genuine problem - return - - # OK, we now have a list of requirements that can't all be - # satisfied at once. - - return self.triggers_message(backtracking_causes) + self.causes_message( - constraints, backtracking_causes - ) - @staticmethod def causes_message(constraints, failure_causes): msg = "\nThe conflict is caused by:" @@ -724,3 +709,18 @@ def extract_requires_python_causes(self, failure_causes): if isinstance(cause.requirement, RequiresPythonRequirement) and not cause.requirement.is_satisfied_by(self._python_candidate) ] + + def get_backtracking_reason_message(self, backtracking_causes, constraints): + requires_python_causes = self.extract_requires_python_causes( + backtracking_causes + ) + if requires_python_causes or len(backtracking_causes) == 1: + # no message when python causes or a single failure, since this is probably a genuine problem + return + + # OK, we now have a list of requirements that can't all be + # satisfied at once. + + return self.triggers_message(backtracking_causes) + self.causes_message( + constraints, backtracking_causes + ) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 934f345f96c..bfef2da7c57 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -28,6 +28,7 @@ def assert_editable(script, *args): @pytest.fixture() def make_fake_wheel(script): + def _make_fake_wheel(name, version, wheel_tag): wheel_house = script.scratch_path.joinpath("wheelhouse") wheel_house.mkdir() @@ -342,10 +343,10 @@ def test_new_resolver_installs_editable(script): ], ) def test_new_resolver_requires_python( - script, - requires_python, - ignore_requires_python, - dep_version, + script, + requires_python, + ignore_requires_python, + dep_version, ): create_basic_wheel_for_package( script, @@ -592,10 +593,10 @@ def test_new_resolver_force_reinstall(script): ids=["default", "exact-pre", "explicit-pre", "no-stable"], ) def test_new_resolver_handles_prerelease( - script, - available_versions, - pip_args, - expected_version, + script, + available_versions, + pip_args, + expected_version, ): for version in available_versions: create_basic_wheel_for_package(script, "pkg", version) @@ -686,16 +687,16 @@ def test_new_resolver_constraint_no_specifier(script): "constraint, error", [ ( - "dist.zip", - "Unnamed requirements are not allowed as constraints", + "dist.zip", + "Unnamed requirements are not allowed as constraints", ), ( - "-e git+https://example.com/dist.git#egg=req", - "Editable requirements are not allowed as constraints", + "-e git+https://example.com/dist.git#egg=req", + "Editable requirements are not allowed as constraints", ), ( - "pkg[extra]", - "Constraints cannot have extras", + "pkg[extra]", + "Constraints cannot have extras", ), ], ) @@ -747,10 +748,10 @@ def test_new_resolver_constraint_on_dependency(script): ], ) def test_new_resolver_constraint_on_path_empty( - script, - constraint_version, - expect_error, - message, + script, + constraint_version, + expect_error, + message, ): """A path requirement can be filtered by a constraint.""" setup_py = script.scratch_path / "setup.py" @@ -1118,7 +1119,7 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): @pytest.mark.parametrize("N", [2, 10, 20]) def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): # Generate a set of wheels that will definitely cause backtracking. - for index in range(1, N + 1): + for index in range(1, N+1): A_version = f"{index}.0.0" B_version = f"{index}.0.0" C_version = "{index_minus_one}.0.0".format(index_minus_one=index - 1) @@ -1130,7 +1131,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): print("A", A_version, "B", B_version, "C", C_version) create_basic_wheel_for_package(script, "A", A_version, depends=depends) - for index in range(1, N + 1): + for index in range(1, N+1): B_version = f"{index}.0.0" C_version = f"{index}.0.0" depends = ["C == " + C_version] @@ -1138,7 +1139,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): print("B", B_version, "C", C_version) create_basic_wheel_for_package(script, "B", B_version, depends=depends) - for index in range(1, N + 1): + for index in range(1, N+1): C_version = f"{index}.0.0" print("C", C_version) create_basic_wheel_for_package(script, "C", C_version) @@ -1216,9 +1217,9 @@ def test_new_resolver_presents_messages_when_backtracking_starts(script): ids=["file_dot", "file_underscore"], ) def test_new_resolver_check_wheel_version_normalized( - script, - metadata_version, - filename_version, + script, + metadata_version, + filename_version, ): filename = f"simple-{filename_version}-py2.py3-none-any.whl" @@ -1605,8 +1606,8 @@ def test_new_resolver_fails_with_url_constraint_and_incompatible_version( assert "Cannot install test_pkg" in result.stderr, str(result) assert ( - "because these package versions have conflicting dependencies." - ) in result.stderr, str(result) + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1696,9 +1697,9 @@ def test_new_resolver_fails_on_needed_conflicting_constraints(script): ) assert ( - "Cannot install test_pkg because these package versions have conflicting " - "dependencies." - ) in result.stderr, str(result) + "Cannot install test_pkg because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1742,8 +1743,8 @@ def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script): assert "Cannot install test-pkg 0.2.0" in result.stderr, str(result) assert ( - "because these package versions have conflicting dependencies." - ) in result.stderr, str(result) + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) script.assert_not_installed("test_pkg") @@ -1831,7 +1832,7 @@ def test_new_resolver_applies_url_constraint_to_dep(script): def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1870,7 +1871,7 @@ def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url( def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1895,15 +1896,15 @@ def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url( ) assert ( - "Cannot install base because these package versions have conflicting " - "dependencies." - ) in result.stderr, str(result) + "Cannot install base because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) script.assert_not_installed("base") def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( - script, make_fake_wheel + script, make_fake_wheel ): initial_path = make_fake_wheel("dep", "0.1.0", "fakepy1-fakeabi-fakeplat") @@ -1998,11 +1999,11 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( ], ) def test_new_resolver_direct_url_equivalent( - tmp_path, - script, - suffixes_equivalent, - depend_suffix, - request_suffix, + tmp_path, + script, + suffixes_equivalent, + depend_suffix, + request_suffix, ): pkga = create_basic_wheel_for_package(script, name="pkga", version="1") pkgb = create_basic_wheel_for_package( From 962ccfabbf9887e60daea75d6d75cba487dbb32f Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Sun, 24 Oct 2021 15:24:35 +0300 Subject: [PATCH 04/11] using the new name for the stage --- src/pip/_internal/resolution/resolvelib/factory.py | 12 ++++-------- src/pip/_internal/resolution/resolvelib/reporter.py | 12 ++++++------ src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- tests/functional/test_new_resolver.py | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0f1199f6005..1016a120798 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -710,17 +710,13 @@ def extract_requires_python_causes(self, failure_causes): and not cause.requirement.is_satisfied_by(self._python_candidate) ] - def get_backtracking_reason_message(self, backtracking_causes, constraints): - requires_python_causes = self.extract_requires_python_causes( - backtracking_causes - ) - if requires_python_causes or len(backtracking_causes) == 1: + def get_conflict_message(self, causes, constraints): + requires_python_causes = self.extract_requires_python_causes(causes) + if requires_python_causes or len(causes) == 1: # no message when python causes or a single failure, since this is probably a genuine problem return # OK, we now have a list of requirements that can't all be # satisfied at once. - return self.triggers_message(backtracking_causes) + self.causes_message( - constraints, backtracking_causes - ) + return self.triggers_message(causes) + self.causes_message(constraints, causes) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index f8195bee47d..c571dfeddc6 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -10,9 +10,9 @@ class PipReporter(BaseReporter): - def __init__(self, backtracking_message_generator) -> None: + def __init__(self, conflicts_message_generator) -> None: self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) - self.backtracking_message_generator = backtracking_message_generator + self.conflicts_message_generator = conflicts_message_generator self._messages_at_backtrack = { 1: ( @@ -43,8 +43,8 @@ def backtracking(self, candidate: Candidate) -> None: message = self._messages_at_backtrack[count] logger.info("INFO: %s", message.format(package_name=candidate.name)) - def start_backtracking(self, causes: Any) -> None: - logger.info("INFO: %s", self.backtracking_message_generator(causes)) + def resolving_conflicts(self, causes: Any) -> None: + logger.info("INFO: %s", self.conflicts_message_generator(causes)) class PipDebuggingReporter(BaseReporter): @@ -68,8 +68,8 @@ def adding_requirement(self, requirement: Requirement, parent: Candidate) -> Non def backtracking(self, candidate: Candidate) -> None: logger.info("Reporter.backtracking(%r)", candidate) - def start_backtracking(self, cause: Any) -> None: - logger.info("Reporter.start_backtracking(%r)", cause) + def resolving_conflicts(self, causes: Any) -> None: + logger.info("Reporter.resolving_conflicts(%r)", causes) def pinning(self, candidate: Candidate) -> None: logger.info("Reporter.pinning(%r)", candidate) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 8cc398f916e..88a375292fd 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -83,7 +83,7 @@ def resolve( else: reporter = PipReporter( functools.partial( - self.factory.get_backtracking_reason_message, + self.factory.get_conflict_message, constraints=collected.constraints, ) ) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index bfef2da7c57..a0973b40ded 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1164,7 +1164,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): assert "press Ctrl + C" in result.stdout -def test_new_resolver_presents_messages_when_backtracking_starts(script): +def test_new_resolver_presents_messages_when_resolving_conflicts(script): packages = [ ("a", "2.0.0", ["c==2.0.0"]), ("a", "1.0.0", ["c==1.0.0"]), @@ -1185,7 +1185,7 @@ def test_new_resolver_presents_messages_when_backtracking_starts(script): "a", "b" ) - assert_installed(script, A="1.0.0", B="1.0.0", C="1.0.0") + script.assert_installed(A="1.0.0", B="1.0.0", C="1.0.0") message = """INFO: Cannot install a==2.0.0 and b==1.0.0 because these package versions have conflicting dependencies. The conflict is caused by: a 2.0.0 depends on c==2.0.0 From b635b2ad44ec95f8c7cbd80d29f9a1cab8f028a7 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Sun, 24 Oct 2021 15:31:52 +0300 Subject: [PATCH 05/11] news file --- news/10210.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/10210.feature.rst diff --git a/news/10210.feature.rst b/news/10210.feature.rst new file mode 100644 index 00000000000..5ec6940b84b --- /dev/null +++ b/news/10210.feature.rst @@ -0,0 +1 @@ +found conflicts are output before pip delves into version resolution. From 65c7506450ebcfd69f9ae1acb9754164d595ef38 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Sun, 24 Oct 2021 15:35:49 +0300 Subject: [PATCH 06/11] fixing format --- tests/functional/test_new_resolver.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index a0973b40ded..2297b8d8c50 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -28,7 +28,6 @@ def assert_editable(script, *args): @pytest.fixture() def make_fake_wheel(script): - def _make_fake_wheel(name, version, wheel_tag): wheel_house = script.scratch_path.joinpath("wheelhouse") wheel_house.mkdir() @@ -1119,7 +1118,7 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): @pytest.mark.parametrize("N", [2, 10, 20]) def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): # Generate a set of wheels that will definitely cause backtracking. - for index in range(1, N+1): + for index in range(1, N + 1): A_version = f"{index}.0.0" B_version = f"{index}.0.0" C_version = "{index_minus_one}.0.0".format(index_minus_one=index - 1) @@ -1131,7 +1130,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): print("A", A_version, "B", B_version, "C", C_version) create_basic_wheel_for_package(script, "A", A_version, depends=depends) - for index in range(1, N+1): + for index in range(1, N + 1): B_version = f"{index}.0.0" C_version = f"{index}.0.0" depends = ["C == " + C_version] @@ -1139,7 +1138,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): print("B", B_version, "C", C_version) create_basic_wheel_for_package(script, "B", B_version, depends=depends) - for index in range(1, N+1): + for index in range(1, N + 1): C_version = f"{index}.0.0" print("C", C_version) create_basic_wheel_for_package(script, "C", C_version) @@ -1181,8 +1180,10 @@ def test_new_resolver_presents_messages_when_resolving_conflicts(script): "install", "--no-cache-dir", "--no-index", - "--find-links", script.scratch_path, - "a", "b" + "--find-links", + script.scratch_path, + "a", + "b", ) script.assert_installed(A="1.0.0", B="1.0.0", C="1.0.0") From 7d3670d00006e90156583f0f11759b9fab094f3a Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Sun, 24 Oct 2021 15:51:22 +0300 Subject: [PATCH 07/11] fixing mypy and black --- .../resolution/resolvelib/factory.py | 32 ++++++++++++------- .../resolution/resolvelib/reporter.py | 6 ++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 1016a120798..f2b0abeeee9 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -47,6 +47,8 @@ from pip._internal.utils.hashes import Hashes from pip._internal.utils.packaging import get_requirement from pip._internal.utils.virtualenv import running_under_virtualenv +from pip._vendor.resolvelib.resolvers import RequirementInformation +from pip._vendor.resolvelib.structs import RT, CT from .base import Candidate, CandidateVersion, Constraint, Requirement from .candidates import ( @@ -78,6 +80,7 @@ class ConflictCause(Protocol): C = TypeVar("C") Cache = Dict[Link, C] +Constraints = Dict[str, Constraint] class CollectedRootRequirements(NamedTuple): @@ -600,7 +603,7 @@ def _report_single_requirement_conflict( def get_installation_error( self, e: "ResolutionImpossible[Requirement, Candidate]", - constraints: Dict[str, Constraint], + constraints: Constraints, ) -> InstallationError: failure_causes = e.causes @@ -632,7 +635,7 @@ def get_installation_error( logger.critical(self.triggers_message(failure_causes)) msg = ( - self.causes_message(constraints, failure_causes) + self.causes_message(failure_causes, constraints) + "\n\n" + "To fix this you could try to:\n" + "1. loosen the range of package versions you've specified\n" @@ -649,10 +652,12 @@ def get_installation_error( ) @staticmethod - def causes_message(constraints, failure_causes): + def causes_message( + causes: List[RequirementInformation[RT, CT]], constraints: Constraints + ) -> str: msg = "\nThe conflict is caused by:" relevant_constraints = set() - for req, parent in failure_causes: + for req, parent in causes: if req.name in constraints: relevant_constraints.add(req.name) msg = msg + "\n " @@ -667,7 +672,7 @@ def causes_message(constraints, failure_causes): return msg @staticmethod - def triggers_message(failure_causes): + def triggers_message(causes: List[RequirementInformation[RT, CT]]) -> str: # A couple of formatting helpers def text_join(parts: List[str]) -> str: if len(parts) == 1: @@ -684,7 +689,7 @@ def describe_trigger(parent: Candidate) -> str: return str(ireq.comes_from) triggers = set() - for req, parent in failure_causes: + for req, parent in causes: if parent is None: # This is a root requirement, so we can report it directly trigger = req.format_for_error() @@ -701,22 +706,25 @@ def describe_trigger(parent: Candidate) -> str: ) return msg - - def extract_requires_python_causes(self, failure_causes): + def extract_requires_python_causes( + self, causes: List[RequirementInformation[RT, CT]] + ) -> List[RequirementInformation[RT, CT]]: return [ cause - for cause in failure_causes + for cause in causes if isinstance(cause.requirement, RequiresPythonRequirement) and not cause.requirement.is_satisfied_by(self._python_candidate) ] - def get_conflict_message(self, causes, constraints): + def get_conflict_message( + self, causes: List[RequirementInformation[RT, CT]], constraints: Constraints + ) -> Optional[str]: requires_python_causes = self.extract_requires_python_causes(causes) if requires_python_causes or len(causes) == 1: # no message when python causes or a single failure, since this is probably a genuine problem - return + return None # OK, we now have a list of requirements that can't all be # satisfied at once. - return self.triggers_message(causes) + self.causes_message(constraints, causes) + return self.triggers_message(causes) + self.causes_message(causes, constraints) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index c571dfeddc6..4f6b1eb793b 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,8 +1,10 @@ from collections import defaultdict from logging import getLogger -from typing import Any, DefaultDict +from typing import Any, DefaultDict, Callable, List, Optional from pip._vendor.resolvelib.reporters import BaseReporter +from pip._vendor.resolvelib.resolvers import RequirementInformation +from pip._vendor.resolvelib.structs import RT, CT from .base import Candidate, Requirement @@ -10,7 +12,7 @@ class PipReporter(BaseReporter): - def __init__(self, conflicts_message_generator) -> None: + def __init__(self, conflicts_message_generator: Callable[[List[List[RequirementInformation[RT, CT]]]], Optional[str]]) -> None: self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) self.conflicts_message_generator = conflicts_message_generator From 40a350454526630d9eaa46b4a91807ee03829635 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 25 Oct 2021 09:50:18 +0300 Subject: [PATCH 08/11] fixing types and linting --- .../_internal/resolution/resolvelib/factory.py | 16 ++++++---------- .../_internal/resolution/resolvelib/reporter.py | 10 +++++++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f2b0abeeee9..26b01653329 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -22,6 +22,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.resolvelib import ResolutionImpossible +from pip._vendor.resolvelib.resolvers import RequirementInformation from pip._internal.cache import CacheEntry, WheelCache from pip._internal.exceptions import ( @@ -47,8 +48,6 @@ from pip._internal.utils.hashes import Hashes from pip._internal.utils.packaging import get_requirement from pip._internal.utils.virtualenv import running_under_virtualenv -from pip._vendor.resolvelib.resolvers import RequirementInformation -from pip._vendor.resolvelib.structs import RT, CT from .base import Candidate, CandidateVersion, Constraint, Requirement from .candidates import ( @@ -81,6 +80,7 @@ class ConflictCause(Protocol): C = TypeVar("C") Cache = Dict[Link, C] Constraints = Dict[str, Constraint] +Causes = Sequence[RequirementInformation[Requirement, Candidate]] class CollectedRootRequirements(NamedTuple): @@ -652,9 +652,7 @@ def get_installation_error( ) @staticmethod - def causes_message( - causes: List[RequirementInformation[RT, CT]], constraints: Constraints - ) -> str: + def causes_message(causes: Causes, constraints: Constraints) -> str: msg = "\nThe conflict is caused by:" relevant_constraints = set() for req, parent in causes: @@ -672,7 +670,7 @@ def causes_message( return msg @staticmethod - def triggers_message(causes: List[RequirementInformation[RT, CT]]) -> str: + def triggers_message(causes: Causes) -> str: # A couple of formatting helpers def text_join(parts: List[str]) -> str: if len(parts) == 1: @@ -706,9 +704,7 @@ def describe_trigger(parent: Candidate) -> str: ) return msg - def extract_requires_python_causes( - self, causes: List[RequirementInformation[RT, CT]] - ) -> List[RequirementInformation[RT, CT]]: + def extract_requires_python_causes(self, causes: Causes) -> Causes: return [ cause for cause in causes @@ -717,7 +713,7 @@ def extract_requires_python_causes( ] def get_conflict_message( - self, causes: List[RequirementInformation[RT, CT]], constraints: Constraints + self, causes: Causes, constraints: Constraints ) -> Optional[str]: requires_python_causes = self.extract_requires_python_causes(causes) if requires_python_causes or len(causes) == 1: diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 4f6b1eb793b..2512b996bf0 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,18 +1,22 @@ from collections import defaultdict from logging import getLogger -from typing import Any, DefaultDict, Callable, List, Optional +from typing import Any, Callable, DefaultDict, List, Optional from pip._vendor.resolvelib.reporters import BaseReporter from pip._vendor.resolvelib.resolvers import RequirementInformation -from pip._vendor.resolvelib.structs import RT, CT from .base import Candidate, Requirement +Causes = List[RequirementInformation[Requirement, Candidate]] + + logger = getLogger(__name__) class PipReporter(BaseReporter): - def __init__(self, conflicts_message_generator: Callable[[List[List[RequirementInformation[RT, CT]]]], Optional[str]]) -> None: + def __init__( + self, conflicts_message_generator: Callable[[Causes], Optional[str]] + ) -> None: self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) self.conflicts_message_generator = conflicts_message_generator From 5437ffc343a62acdaddd68bc110cbde738f9d928 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 25 Oct 2021 10:03:34 +0300 Subject: [PATCH 09/11] fixing flake8 --- src/pip/_internal/resolution/resolvelib/factory.py | 7 +++++-- src/pip/_internal/resolution/resolvelib/reporter.py | 7 +++++-- tests/functional/test_new_resolver.py | 11 +++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 26b01653329..f5816e73071 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -74,13 +74,15 @@ class ConflictCause(Protocol): requirement: RequiresPythonRequirement parent: Candidate + Causes = Sequence[RequirementInformation[Requirement, Candidate]] +else: + Causes = Sequence logger = logging.getLogger(__name__) C = TypeVar("C") Cache = Dict[Link, C] Constraints = Dict[str, Constraint] -Causes = Sequence[RequirementInformation[Requirement, Candidate]] class CollectedRootRequirements(NamedTuple): @@ -717,7 +719,8 @@ def get_conflict_message( ) -> Optional[str]: requires_python_causes = self.extract_requires_python_causes(causes) if requires_python_causes or len(causes) == 1: - # no message when python causes or a single failure, since this is probably a genuine problem + # no message when python causes or a single failure + # since this is probably a genuine problem return None # OK, we now have a list of requirements that can't all be diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 2512b996bf0..bd008f58b3e 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,13 +1,16 @@ from collections import defaultdict from logging import getLogger -from typing import Any, Callable, DefaultDict, List, Optional +from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Optional, Sequence from pip._vendor.resolvelib.reporters import BaseReporter from pip._vendor.resolvelib.resolvers import RequirementInformation from .base import Candidate, Requirement -Causes = List[RequirementInformation[Requirement, Candidate]] +if TYPE_CHECKING: + Causes = Sequence[RequirementInformation[Requirement, Candidate]] +else: + Causes = Sequence logger = getLogger(__name__) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 2297b8d8c50..03261febe0c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1187,10 +1187,13 @@ def test_new_resolver_presents_messages_when_resolving_conflicts(script): ) script.assert_installed(A="1.0.0", B="1.0.0", C="1.0.0") - message = """INFO: Cannot install a==2.0.0 and b==1.0.0 because these package versions have conflicting dependencies. -The conflict is caused by: - a 2.0.0 depends on c==2.0.0 - b 1.0.0 depends on c==1.0.0""" + message = ( + "INFO: Cannot install a==2.0.0 and b==1.0.0 " + "because these package versions have conflicting dependencies.\n" + "The conflict is caused by:\n" + " a 2.0.0 depends on c==2.0.0\n" + " b 1.0.0 depends on c==1.0.0" + ) stdout = result.stdout assert message in stdout From 0fc062a716215465343f9bab66096649cbf14f25 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Mon, 8 Nov 2021 12:06:56 +0200 Subject: [PATCH 10/11] showing message only once --- .../resolution/resolvelib/reporter.py | 7 +++-- tests/functional/test_new_resolver.py | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index bd008f58b3e..7900dbab457 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -20,8 +20,9 @@ class PipReporter(BaseReporter): def __init__( self, conflicts_message_generator: Callable[[Causes], Optional[str]] ) -> None: - self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) self.conflicts_message_generator = conflicts_message_generator + self.resolved_conflicts = False + self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) self._messages_at_backtrack = { 1: ( @@ -53,7 +54,9 @@ def backtracking(self, candidate: Candidate) -> None: logger.info("INFO: %s", message.format(package_name=candidate.name)) def resolving_conflicts(self, causes: Any) -> None: - logger.info("INFO: %s", self.conflicts_message_generator(causes)) + if not self.resolved_conflicts: + self.resolved_conflicts = True + logger.info("INFO: %s", self.conflicts_message_generator(causes)) class PipDebuggingReporter(BaseReporter): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 03261febe0c..8fab23cd11a 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1163,13 +1163,26 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): assert "press Ctrl + C" in result.stdout -def test_new_resolver_presents_messages_when_resolving_conflicts(script): +def test_new_resolver_presents_conflicts_when_resolving_conflicts_for_the_first_time( + script, +): + def conflict_message(v): + return ( + f"INFO: Cannot install a=={v}.0.0 and b==1.0.0 " + f"because these package versions have conflicting dependencies.\n" + f"The conflict is caused by:\n" + f" a {v}.0.0 depends on c=={v}.0.0\n" + f" b 1.0.0 depends on c==1.0.0" + ) + packages = [ + ("a", "3.0.0", ["c==3.0.0"]), ("a", "2.0.0", ["c==2.0.0"]), ("a", "1.0.0", ["c==1.0.0"]), ("b", "1.0.0", ["c==1.0.0"]), ("c", "1.0.0", []), ("c", "2.0.0", []), + ("c", "3.0.0", []), ] for name, version, depends in packages: @@ -1187,15 +1200,12 @@ def test_new_resolver_presents_messages_when_resolving_conflicts(script): ) script.assert_installed(A="1.0.0", B="1.0.0", C="1.0.0") - message = ( - "INFO: Cannot install a==2.0.0 and b==1.0.0 " - "because these package versions have conflicting dependencies.\n" - "The conflict is caused by:\n" - " a 2.0.0 depends on c==2.0.0\n" - " b 1.0.0 depends on c==1.0.0" - ) + first_message = conflict_message(3) + second_message = conflict_message(2) + stdout = result.stdout - assert message in stdout + assert first_message in stdout + assert second_message not in stdout @pytest.mark.parametrize( From fba41e7a5f078a14ca1f551ff8373d7e052ae7d4 Mon Sep 17 00:00:00 2001 From: Nadav Wexler Date: Sun, 14 Nov 2021 15:54:45 +0200 Subject: [PATCH 11/11] fixing news fragment --- news/10210.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/10210.feature.rst b/news/10210.feature.rst index 5ec6940b84b..c2d7924e054 100644 --- a/news/10210.feature.rst +++ b/news/10210.feature.rst @@ -1 +1 @@ -found conflicts are output before pip delves into version resolution. +Display found conflicts before pip delves into version resolution.