From 8931c6dbc31dbddd075208d9ac607e27ec20ab39 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 16 Oct 2021 13:19:24 +0200 Subject: [PATCH 01/22] added extended test and applied PoC fix --- src/resolvelib/resolvers.py | 18 ++++++- tests/test_poc.py | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/test_poc.py diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 787681b..458c2c8 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -199,7 +199,23 @@ def _is_current_pin_satisfying(self, name, criterion): ) def _get_updated_criteria(self, candidate): - criteria = self.state.criteria.copy() + # copy current state's criteria, filtering out any information with this + # candidate's other versions as parent + criteria = { + name: Criterion( + criterion.candidates, + [ + information + for information in criterion.information + if ( + information[1] is None + or self._p.identify(information[1]) != self._p.identify(candidate) + ) + ], + criterion.incompatibilities, + ) + for name, criterion in self.state.criteria.items() + } for requirement in self._p.get_dependencies(candidate=candidate): self._add_to_criteria(criteria, requirement, parent=candidate) return criteria diff --git a/tests/test_poc.py b/tests/test_poc.py new file mode 100644 index 0000000..4801d56 --- /dev/null +++ b/tests/test_poc.py @@ -0,0 +1,97 @@ +from pkg_resources import Requirement +from typing import List, Sequence, Set + +from resolvelib import ( + AbstractProvider, + BaseReporter, +) +from resolvelib.resolvers import ( + Criterion, + Resolution, + Resolver, + RequirementInformation, + RequirementsConflicted, +) + + +def test_poc(monkeypatch): + all_candidates = { + "parent": [("parent", "1", ["child<2"])], + "child": [ + ("child", "2", ["grandchild>=2"]), + ("child", "1", ["grandchild<2"]), + ("child", "0.1", ["grandchild"]), + ], + "grandchild": [ + ("grandchild", "2", []), + ("grandchild", "1", []), + ], + } + + class Provider(AbstractProvider): + def identify(self, requirement_or_candidate): + result: str = ( + Requirement.parse(requirement_or_candidate).key + if isinstance(requirement_or_candidate, str) + else requirement_or_candidate[0] + ) + assert result in all_candidates + return result + + def get_preference(self, *, identifier, **_): + # prefer child over parent (alphabetically) + return identifier + + def get_dependencies(self, candidate): + return candidate[2] + + def find_matches(self, identifier, requirements, incompatibilities): + return ( + candidate + for candidate in all_candidates[identifier] + if all( + candidate[1] in Requirement.parse(req) + for req in requirements[identifier] + ) + if candidate not in incompatibilities[identifier] + ) + + def is_satisfied_by(self, requirement, candidate): + return candidate[1] in Requirement.parse(requirement) + + # patch Resolution._get_updated_criteria to collect rejected states + rejected_criteria: List[Criterion] = [] + get_updated_criterion_orig = Resolution._get_updated_criteria + + def get_updated_criterion_patch(self, candidate) -> None: + try: + return get_updated_criterion_orig(self, candidate) + except RequirementsConflicted as e: + rejected_criteria.append(e.criterion) + raise + + monkeypatch.setattr( + Resolution, "_get_updated_criteria", get_updated_criterion_patch + ) + + resolver = Resolver(Provider(), BaseReporter()) + result = resolver.resolve(["child", "parent"]) + + def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str]: + return { + inf[1][1] + for inf in information + if inf[1][0] == "child" + } + + # verify that none of the rejected criteria are based on more than one candidate for + # child + assert not any( + len(get_child_versions(criterion.information)) > 1 + for criterion in rejected_criteria + ) + + assert set(result.mapping) == {"parent", "child", "grandchild"} + assert result.mapping["parent"][1] == "1" + assert result.mapping["child"][1] == "1" + assert result.mapping["grandchild"][1] == "1" From 568fc6d661c6c125b6bb2d686e36f14a472e179c Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 18:39:25 +0200 Subject: [PATCH 02/22] cleaned up fix --- src/resolvelib/resolvers.py | 49 +++++++++++++++++++++++-------------- tests/test_poc.py | 8 ++++-- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 812a614..5922885 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -173,7 +173,27 @@ def _add_to_criteria(self, criteria, requirement, parent): raise RequirementsConflicted(criterion) criteria[identifier] = criterion + def _remove_information_from_citeria(self, criteria, parents): + """ + Removes information from a set of parents from a criteria. Concretely, removes from each criterion's `information` + field all values that have one of `parents` as provider of the requirement. + + :param criteria: The criteria to update. + :param parents: The set of identifiers for which to remove information from all criteria. + """ + for key, criterion in criteria.items(): + # TODO: is empty information allowed? + criterion.information = [ + information + for information in criterion.information + if ( + information[1] is None + or self._p.identify(information[1]) not in parents + ) + ] + def _get_preference(self, name): + # TODO: empty informations bug: verify + test case return self._p.get_preference( identifier=name, resolutions=self.state.mapping, @@ -199,23 +219,7 @@ def _is_current_pin_satisfying(self, name, criterion): ) def _get_updated_criteria(self, candidate): - # copy current state's criteria, filtering out any information with this - # candidate's other versions as parent - criteria = { - name: Criterion( - criterion.candidates, - [ - information - for information in criterion.information - if ( - information[1] is None - or self._p.identify(information[1]) != self._p.identify(candidate) - ) - ], - criterion.incompatibilities, - ) - for name, criterion in self.state.criteria.items() - } + criteria = self.state.criteria.copy() for requirement in self._p.get_dependencies(candidate=candidate): self._add_to_criteria(criteria, requirement, parent=candidate) return criteria @@ -372,11 +376,11 @@ def resolve(self, requirements, max_rounds): for round_index in range(max_rounds): self._r.starting_round(index=round_index) - unsatisfied_names = [ + unsatisfied_names = { key for key, criterion in self.state.criteria.items() if not self._is_current_pin_satisfying(key, criterion) - ] + } # All criteria are accounted for. Nothing more to pin, we are done! if not unsatisfied_names: @@ -400,6 +404,13 @@ def resolve(self, requirements, max_rounds): raise ResolutionImpossible(self.state.backtrack_causes) else: # Pinning was successful. Push a new state to do another pin. + new_unsatisfied_names = { + key + for key, criterion in self.state.criteria.items() + if key not in set(unsatisfied_names) + if not self._is_current_pin_satisfying(key, criterion) + } + self._remove_information_from_citeria(self.state.criteria, new_unsatisfied_names) self._push_new_state() self._r.ending_round(index=round_index, state=self.state) diff --git a/tests/test_poc.py b/tests/test_poc.py index 4801d56..7e10a12 100644 --- a/tests/test_poc.py +++ b/tests/test_poc.py @@ -14,7 +14,7 @@ ) -def test_poc(monkeypatch): +def test_poc(monkeypatch, reporter): all_candidates = { "parent": [("parent", "1", ["child<2"])], "child": [ @@ -74,7 +74,7 @@ def get_updated_criterion_patch(self, candidate) -> None: Resolution, "_get_updated_criteria", get_updated_criterion_patch ) - resolver = Resolver(Provider(), BaseReporter()) + resolver = Resolver(Provider(), reporter) result = resolver.resolve(["child", "parent"]) def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str]: @@ -95,3 +95,7 @@ def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str assert result.mapping["parent"][1] == "1" assert result.mapping["child"][1] == "1" assert result.mapping["grandchild"][1] == "1" + + # TODO: review test case + # TODO: remove + assert False From 525b8c2e6d6fe83614e6dfc91e94be4e16c643d9 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 18:49:10 +0200 Subject: [PATCH 03/22] todo --- tests/test_poc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_poc.py b/tests/test_poc.py index 7e10a12..02b6d8f 100644 --- a/tests/test_poc.py +++ b/tests/test_poc.py @@ -97,5 +97,6 @@ def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str assert result.mapping["grandchild"][1] == "1" # TODO: review test case + # TODO: rename + move test # TODO: remove assert False From 47f1e6c62ee83561bc68cb89cc935f2fc430dae7 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 19:00:11 +0200 Subject: [PATCH 04/22] fixed nondeterminism --- src/resolvelib/resolvers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 5922885..3784c85 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -376,11 +376,11 @@ def resolve(self, requirements, max_rounds): for round_index in range(max_rounds): self._r.starting_round(index=round_index) - unsatisfied_names = { + unsatisfied_names = [ key for key, criterion in self.state.criteria.items() if not self._is_current_pin_satisfying(key, criterion) - } + ] # All criteria are accounted for. Nothing more to pin, we are done! if not unsatisfied_names: @@ -404,13 +404,15 @@ def resolve(self, requirements, max_rounds): raise ResolutionImpossible(self.state.backtrack_causes) else: # Pinning was successful. Push a new state to do another pin. + old_unsatisfied_names = set(unsatisfied_names) new_unsatisfied_names = { key for key, criterion in self.state.criteria.items() - if key not in set(unsatisfied_names) + if key not in old_unsatisfied_names if not self._is_current_pin_satisfying(key, criterion) } - self._remove_information_from_citeria(self.state.criteria, new_unsatisfied_names) + if new_unsatisfied_names: + self._remove_information_from_citeria(self.state.criteria, new_unsatisfied_names) self._push_new_state() self._r.ending_round(index=round_index, state=self.state) From 7586575c74bb3820e5abc8d5eceaa77cfc565116 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 19:12:06 +0200 Subject: [PATCH 05/22] fixed too greedy trimming --- src/resolvelib/resolvers.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 3784c85..eb3160c 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -181,6 +181,8 @@ def _remove_information_from_citeria(self, criteria, parents): :param criteria: The criteria to update. :param parents: The set of identifiers for which to remove information from all criteria. """ + # TODO: remove + print("trimming", parents) for key, criterion in criteria.items(): # TODO: is empty information allowed? criterion.information = [ @@ -387,6 +389,9 @@ def resolve(self, requirements, max_rounds): self._r.ending(state=self.state) return self.state + # keep track of satisfied names to calculate diff ater pinning + satisfied_names = self.state.criteria.keys() - set(unsatisfied_names) + # Choose the most preferred unpinned criterion to try. name = min(unsatisfied_names, key=self._get_preference) failure_causes = self._attempt_to_pin_criterion(name) @@ -403,16 +408,16 @@ def resolve(self, requirements, max_rounds): if not success: raise ResolutionImpossible(self.state.backtrack_causes) else: - # Pinning was successful. Push a new state to do another pin. - old_unsatisfied_names = set(unsatisfied_names) - new_unsatisfied_names = { + # discard as information sources any unsatisfied names that were satisfied before because they are invalid now + newly_unsatisfied_names = { key for key, criterion in self.state.criteria.items() - if key not in old_unsatisfied_names + if key in satisfied_names if not self._is_current_pin_satisfying(key, criterion) } - if new_unsatisfied_names: - self._remove_information_from_citeria(self.state.criteria, new_unsatisfied_names) + if newly_unsatisfied_names: + self._remove_information_from_citeria(self.state.criteria, newly_unsatisfied_names) + # Pinning was successful. Push a new state to do another pin. self._push_new_state() self._r.ending_round(index=round_index, state=self.state) From dabe77d941dee83ae50f25902bf24eee5eed2de7 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 19:32:44 +0200 Subject: [PATCH 06/22] fixed failing test --- src/resolvelib/resolvers.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index eb3160c..bef6b5e 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -173,7 +173,7 @@ def _add_to_criteria(self, criteria, requirement, parent): raise RequirementsConflicted(criterion) criteria[identifier] = criterion - def _remove_information_from_citeria(self, criteria, parents): + def _remove_information_from_criteria(self, criteria, parents): """ Removes information from a set of parents from a criteria. Concretely, removes from each criterion's `information` field all values that have one of `parents` as provider of the requirement. @@ -181,18 +181,23 @@ def _remove_information_from_citeria(self, criteria, parents): :param criteria: The criteria to update. :param parents: The set of identifiers for which to remove information from all criteria. """ - # TODO: remove - print("trimming", parents) + if not parents: + return for key, criterion in criteria.items(): - # TODO: is empty information allowed? - criterion.information = [ - information - for information in criterion.information - if ( - information[1] is None - or self._p.identify(information[1]) not in parents - ) - ] + # TODO: clean up implementation + criteria[key] = Criterion( + criterion.candidates, + # TODO: is empty information allowed? + [ + information + for information in criterion.information + if ( + information[1] is None + or self._p.identify(information[1]) not in parents + ) + ], + criterion.incompatibilities, + ) def _get_preference(self, name): # TODO: empty informations bug: verify + test case @@ -408,15 +413,14 @@ def resolve(self, requirements, max_rounds): if not success: raise ResolutionImpossible(self.state.backtrack_causes) else: - # discard as information sources any unsatisfied names that were satisfied before because they are invalid now + # discard as information sources any invalidated names (unsatisfied names that were previously satisfied) newly_unsatisfied_names = { key for key, criterion in self.state.criteria.items() if key in satisfied_names if not self._is_current_pin_satisfying(key, criterion) } - if newly_unsatisfied_names: - self._remove_information_from_citeria(self.state.criteria, newly_unsatisfied_names) + self._remove_information_from_criteria(self.state.criteria, newly_unsatisfied_names) # Pinning was successful. Push a new state to do another pin. self._push_new_state() From 180915445532c14cbb3bbf39d43ddfa51744c8e8 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 19:37:16 +0200 Subject: [PATCH 07/22] todo --- src/resolvelib/resolvers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index bef6b5e..dd11ab0 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -184,7 +184,6 @@ def _remove_information_from_criteria(self, criteria, parents): if not parents: return for key, criterion in criteria.items(): - # TODO: clean up implementation criteria[key] = Criterion( criterion.candidates, # TODO: is empty information allowed? From 8232a179d2ef868450b063dab6bee0bf625984af Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 22:51:27 +0200 Subject: [PATCH 08/22] cleaned up test case --- tests/test_poc.py | 66 ++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/tests/test_poc.py b/tests/test_poc.py index 02b6d8f..84c77f5 100644 --- a/tests/test_poc.py +++ b/tests/test_poc.py @@ -1,9 +1,9 @@ +from packaging.version import Version from pkg_resources import Requirement -from typing import List, Sequence, Set +from typing import Collection, Iterator, List, Mapping, Sequence, Set, Tuple, Union from resolvelib import ( AbstractProvider, - BaseReporter, ) from resolvelib.resolvers import ( Criterion, @@ -14,50 +14,60 @@ ) -def test_poc(monkeypatch, reporter): - all_candidates = { - "parent": [("parent", "1", ["child<2"])], +def test_pin_conflict_with_self(monkeypatch, reporter) -> None: + """ + Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) + version for that same candidate (#91). + """ + Candidate = Tuple[str, Version, Sequence[str]] # name, version, requirements + all_candidates: Mapping[str, Sequence[Candidate]] = { + "parent": [("parent", Version("1"), ["child<2"])], "child": [ - ("child", "2", ["grandchild>=2"]), - ("child", "1", ["grandchild<2"]), - ("child", "0.1", ["grandchild"]), + ("child", Version("2"), ["grandchild>=2"]), + ("child", Version("1"), ["grandchild<2"]), + ("child", Version("0.1"), ["grandchild"]), ], "grandchild": [ - ("grandchild", "2", []), - ("grandchild", "1", []), + ("grandchild", Version("2"), []), + ("grandchild", Version("1"), []), ], } - class Provider(AbstractProvider): - def identify(self, requirement_or_candidate): + class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] + def identify(self, requirement_or_candidate: Union[str, Candidate]) -> str: result: str = ( Requirement.parse(requirement_or_candidate).key if isinstance(requirement_or_candidate, str) else requirement_or_candidate[0] ) - assert result in all_candidates + assert result in all_candidates, "unknown requirement_or_candidate" return result - def get_preference(self, *, identifier, **_): + def get_preference(self, identifier: str, *args: object, **kwargs: object) -> str: # prefer child over parent (alphabetically) return identifier - def get_dependencies(self, candidate): + def get_dependencies(self, candidate: Candidate) -> Sequence[str]: return candidate[2] - def find_matches(self, identifier, requirements, incompatibilities): + def find_matches( + self, + identifier: str, + requirements: Mapping[str, Iterator[str]], + incompatibilities: Mapping[str, Iterator[Candidate]] + ) -> Iterator[Candidate]: return ( candidate for candidate in all_candidates[identifier] if all( - candidate[1] in Requirement.parse(req) + self.is_satisfied_by(req, candidate) for req in requirements[identifier] ) if candidate not in incompatibilities[identifier] ) - def is_satisfied_by(self, requirement, candidate): - return candidate[1] in Requirement.parse(requirement) + def is_satisfied_by(self, requirement: str, candidate: Candidate) -> bool: + return str(candidate[1]) in Requirement.parse(requirement) # patch Resolution._get_updated_criteria to collect rejected states rejected_criteria: List[Criterion] = [] @@ -74,14 +84,14 @@ def get_updated_criterion_patch(self, candidate) -> None: Resolution, "_get_updated_criteria", get_updated_criterion_patch ) - resolver = Resolver(Provider(), reporter) + resolver: Resolver = Resolver(Provider(), reporter) result = resolver.resolve(["child", "parent"]) - def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str]: + def get_child_versions(information: Collection[RequirementInformation[str, Candidate]]) -> Set[str]: return { - inf[1][1] + str(inf.parent[1]) for inf in information - if inf[1][0] == "child" + if inf.parent is not None and inf.parent[0] == "child" } # verify that none of the rejected criteria are based on more than one candidate for @@ -92,11 +102,9 @@ def get_child_versions(information: Sequence[RequirementInformation]) -> Set[str ) assert set(result.mapping) == {"parent", "child", "grandchild"} - assert result.mapping["parent"][1] == "1" - assert result.mapping["child"][1] == "1" - assert result.mapping["grandchild"][1] == "1" + assert result.mapping["parent"][1] == Version("1") + assert result.mapping["child"][1] == Version("1") + assert result.mapping["grandchild"][1] == Version("1") - # TODO: review test case # TODO: rename + move test - # TODO: remove - assert False + # TODO: style check? From 469e1e5decc2ef349d432681404be826be841ab9 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 22:54:49 +0200 Subject: [PATCH 09/22] todos --- tests/test_poc.py | 110 ---------------------------------------- tests/test_resolvers.py | 102 +++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 110 deletions(-) delete mode 100644 tests/test_poc.py diff --git a/tests/test_poc.py b/tests/test_poc.py deleted file mode 100644 index 84c77f5..0000000 --- a/tests/test_poc.py +++ /dev/null @@ -1,110 +0,0 @@ -from packaging.version import Version -from pkg_resources import Requirement -from typing import Collection, Iterator, List, Mapping, Sequence, Set, Tuple, Union - -from resolvelib import ( - AbstractProvider, -) -from resolvelib.resolvers import ( - Criterion, - Resolution, - Resolver, - RequirementInformation, - RequirementsConflicted, -) - - -def test_pin_conflict_with_self(monkeypatch, reporter) -> None: - """ - Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) - version for that same candidate (#91). - """ - Candidate = Tuple[str, Version, Sequence[str]] # name, version, requirements - all_candidates: Mapping[str, Sequence[Candidate]] = { - "parent": [("parent", Version("1"), ["child<2"])], - "child": [ - ("child", Version("2"), ["grandchild>=2"]), - ("child", Version("1"), ["grandchild<2"]), - ("child", Version("0.1"), ["grandchild"]), - ], - "grandchild": [ - ("grandchild", Version("2"), []), - ("grandchild", Version("1"), []), - ], - } - - class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] - def identify(self, requirement_or_candidate: Union[str, Candidate]) -> str: - result: str = ( - Requirement.parse(requirement_or_candidate).key - if isinstance(requirement_or_candidate, str) - else requirement_or_candidate[0] - ) - assert result in all_candidates, "unknown requirement_or_candidate" - return result - - def get_preference(self, identifier: str, *args: object, **kwargs: object) -> str: - # prefer child over parent (alphabetically) - return identifier - - def get_dependencies(self, candidate: Candidate) -> Sequence[str]: - return candidate[2] - - def find_matches( - self, - identifier: str, - requirements: Mapping[str, Iterator[str]], - incompatibilities: Mapping[str, Iterator[Candidate]] - ) -> Iterator[Candidate]: - return ( - candidate - for candidate in all_candidates[identifier] - if all( - self.is_satisfied_by(req, candidate) - for req in requirements[identifier] - ) - if candidate not in incompatibilities[identifier] - ) - - def is_satisfied_by(self, requirement: str, candidate: Candidate) -> bool: - return str(candidate[1]) in Requirement.parse(requirement) - - # patch Resolution._get_updated_criteria to collect rejected states - rejected_criteria: List[Criterion] = [] - get_updated_criterion_orig = Resolution._get_updated_criteria - - def get_updated_criterion_patch(self, candidate) -> None: - try: - return get_updated_criterion_orig(self, candidate) - except RequirementsConflicted as e: - rejected_criteria.append(e.criterion) - raise - - monkeypatch.setattr( - Resolution, "_get_updated_criteria", get_updated_criterion_patch - ) - - resolver: Resolver = Resolver(Provider(), reporter) - result = resolver.resolve(["child", "parent"]) - - def get_child_versions(information: Collection[RequirementInformation[str, Candidate]]) -> Set[str]: - return { - str(inf.parent[1]) - for inf in information - if inf.parent is not None and inf.parent[0] == "child" - } - - # verify that none of the rejected criteria are based on more than one candidate for - # child - assert not any( - len(get_child_versions(criterion.information)) > 1 - for criterion in rejected_criteria - ) - - assert set(result.mapping) == {"parent", "child", "grandchild"} - assert result.mapping["parent"][1] == Version("1") - assert result.mapping["child"][1] == Version("1") - assert result.mapping["grandchild"][1] == Version("1") - - # TODO: rename + move test - # TODO: style check? diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 8af925a..102915f 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -1,4 +1,7 @@ import pytest +from packaging.version import Version +from pkg_resources import Requirement +from typing import Collection, Iterator, List, Mapping, Sequence, Set, Tuple, Union from resolvelib import ( AbstractProvider, @@ -7,6 +10,12 @@ ResolutionImpossible, Resolver, ) +from resolvelib.resolvers import ( + Criterion, + Resolution, + RequirementInformation, + RequirementsConflicted, +) def test_candidate_inconsistent_error(): @@ -143,3 +152,96 @@ def run_resolver(*args): backtracking_causes = run_resolver([("a", {1, 2}), ("b", {1})]) exception_causes = run_resolver([("a", {2}), ("b", {1})]) assert exception_causes == backtracking_causes + + +def test_pin_conflict_with_self(monkeypatch, reporter) -> None: + """ + Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) + version for that same candidate (#91). + """ + Candidate = Tuple[str, Version, Sequence[str]] # name, version, requirements + all_candidates: Mapping[str, Sequence[Candidate]] = { + "parent": [("parent", Version("1"), ["child<2"])], + "child": [ + ("child", Version("2"), ["grandchild>=2"]), + ("child", Version("1"), ["grandchild<2"]), + ("child", Version("0.1"), ["grandchild"]), + ], + "grandchild": [ + ("grandchild", Version("2"), []), + ("grandchild", Version("1"), []), + ], + } + + class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] + def identify(self, requirement_or_candidate: Union[str, Candidate]) -> str: + result: str = ( + Requirement.parse(requirement_or_candidate).key + if isinstance(requirement_or_candidate, str) + else requirement_or_candidate[0] + ) + assert result in all_candidates, "unknown requirement_or_candidate" + return result + + def get_preference(self, identifier: str, *args: object, **kwargs: object) -> str: + # prefer child over parent (alphabetically) + return identifier + + def get_dependencies(self, candidate: Candidate) -> Sequence[str]: + return candidate[2] + + def find_matches( + self, + identifier: str, + requirements: Mapping[str, Iterator[str]], + incompatibilities: Mapping[str, Iterator[Candidate]] + ) -> Iterator[Candidate]: + return ( + candidate + for candidate in all_candidates[identifier] + if all( + self.is_satisfied_by(req, candidate) + for req in requirements[identifier] + ) + if candidate not in incompatibilities[identifier] + ) + + def is_satisfied_by(self, requirement: str, candidate: Candidate) -> bool: + return str(candidate[1]) in Requirement.parse(requirement) + + # patch Resolution._get_updated_criteria to collect rejected states + rejected_criteria: List[Criterion] = [] + get_updated_criterion_orig = Resolution._get_updated_criteria + + def get_updated_criterion_patch(self, candidate) -> None: + try: + return get_updated_criterion_orig(self, candidate) + except RequirementsConflicted as e: + rejected_criteria.append(e.criterion) + raise + + monkeypatch.setattr( + Resolution, "_get_updated_criteria", get_updated_criterion_patch + ) + + resolver: Resolver = Resolver(Provider(), reporter) + result = resolver.resolve(["child", "parent"]) + + def get_child_versions(information: Collection[RequirementInformation[str, Candidate]]) -> Set[str]: + return { + str(inf.parent[1]) + for inf in information + if inf.parent is not None and inf.parent[0] == "child" + } + + # verify that none of the rejected criteria are based on more than one candidate for + # child + assert not any( + len(get_child_versions(criterion.information)) > 1 + for criterion in rejected_criteria + ) + + assert set(result.mapping) == {"parent", "child", "grandchild"} + assert result.mapping["parent"][1] == Version("1") + assert result.mapping["child"][1] == Version("1") + assert result.mapping["grandchild"][1] == Version("1") From 41ceb33cbbae584e9ba358e5743082a9c9a399f1 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 22:56:53 +0200 Subject: [PATCH 10/22] black --- src/resolvelib/resolvers.py | 8 ++++++-- tests/test_resolvers.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index dd11ab0..3581955 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -394,7 +394,9 @@ def resolve(self, requirements, max_rounds): return self.state # keep track of satisfied names to calculate diff ater pinning - satisfied_names = self.state.criteria.keys() - set(unsatisfied_names) + satisfied_names = self.state.criteria.keys() - set( + unsatisfied_names + ) # Choose the most preferred unpinned criterion to try. name = min(unsatisfied_names, key=self._get_preference) @@ -419,7 +421,9 @@ def resolve(self, requirements, max_rounds): if key in satisfied_names if not self._is_current_pin_satisfying(key, criterion) } - self._remove_information_from_criteria(self.state.criteria, newly_unsatisfied_names) + self._remove_information_from_criteria( + self.state.criteria, newly_unsatisfied_names + ) # Pinning was successful. Push a new state to do another pin. self._push_new_state() diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 102915f..a74718b 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -1,7 +1,16 @@ import pytest from packaging.version import Version from pkg_resources import Requirement -from typing import Collection, Iterator, List, Mapping, Sequence, Set, Tuple, Union +from typing import ( + Collection, + Iterator, + List, + Mapping, + Sequence, + Set, + Tuple, + Union, +) from resolvelib import ( AbstractProvider, @@ -159,7 +168,9 @@ def test_pin_conflict_with_self(monkeypatch, reporter) -> None: Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) version for that same candidate (#91). """ - Candidate = Tuple[str, Version, Sequence[str]] # name, version, requirements + Candidate = Tuple[ + str, Version, Sequence[str] + ] # name, version, requirements all_candidates: Mapping[str, Sequence[Candidate]] = { "parent": [("parent", Version("1"), ["child<2"])], "child": [ @@ -174,7 +185,9 @@ def test_pin_conflict_with_self(monkeypatch, reporter) -> None: } class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] - def identify(self, requirement_or_candidate: Union[str, Candidate]) -> str: + def identify( + self, requirement_or_candidate: Union[str, Candidate] + ) -> str: result: str = ( Requirement.parse(requirement_or_candidate).key if isinstance(requirement_or_candidate, str) @@ -183,7 +196,9 @@ def identify(self, requirement_or_candidate: Union[str, Candidate]) -> str: assert result in all_candidates, "unknown requirement_or_candidate" return result - def get_preference(self, identifier: str, *args: object, **kwargs: object) -> str: + def get_preference( + self, identifier: str, *args: object, **kwargs: object + ) -> str: # prefer child over parent (alphabetically) return identifier @@ -194,7 +209,7 @@ def find_matches( self, identifier: str, requirements: Mapping[str, Iterator[str]], - incompatibilities: Mapping[str, Iterator[Candidate]] + incompatibilities: Mapping[str, Iterator[Candidate]], ) -> Iterator[Candidate]: return ( candidate @@ -206,7 +221,9 @@ def find_matches( if candidate not in incompatibilities[identifier] ) - def is_satisfied_by(self, requirement: str, candidate: Candidate) -> bool: + def is_satisfied_by( + self, requirement: str, candidate: Candidate + ) -> bool: return str(candidate[1]) in Requirement.parse(requirement) # patch Resolution._get_updated_criteria to collect rejected states @@ -227,7 +244,9 @@ def get_updated_criterion_patch(self, candidate) -> None: resolver: Resolver = Resolver(Provider(), reporter) result = resolver.resolve(["child", "parent"]) - def get_child_versions(information: Collection[RequirementInformation[str, Candidate]]) -> Set[str]: + def get_child_versions( + information: Collection[RequirementInformation[str, Candidate]] + ) -> Set[str]: return { str(inf.parent[1]) for inf in information From 8e4d2c4157bac9165c92407990b3235e92fd1846 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 22:58:36 +0200 Subject: [PATCH 11/22] pep8 --- src/resolvelib/resolvers.py | 11 +++++++---- tests/test_resolvers.py | 13 +++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 3581955..4faa6d4 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -175,11 +175,13 @@ def _add_to_criteria(self, criteria, requirement, parent): def _remove_information_from_criteria(self, criteria, parents): """ - Removes information from a set of parents from a criteria. Concretely, removes from each criterion's `information` - field all values that have one of `parents` as provider of the requirement. + Removes information from a set of parents from a criteria. Concretely, removes + from each criterion's `information` field all values that have one of `parents` + as provider of the requirement. :param criteria: The criteria to update. - :param parents: The set of identifiers for which to remove information from all criteria. + :param parents: The set of identifiers for which to remove information from all + criteria. """ if not parents: return @@ -414,7 +416,8 @@ def resolve(self, requirements, max_rounds): if not success: raise ResolutionImpossible(self.state.backtrack_causes) else: - # discard as information sources any invalidated names (unsatisfied names that were previously satisfied) + # discard as information sources any invalidated names + # (unsatisfied names that were previously satisfied) newly_unsatisfied_names = { key for key, criterion in self.state.criteria.items() diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index a74718b..cbadfe7 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -1,6 +1,3 @@ -import pytest -from packaging.version import Version -from pkg_resources import Requirement from typing import ( Collection, Iterator, @@ -12,6 +9,10 @@ Union, ) +import pytest +from packaging.version import Version +from pkg_resources import Requirement + from resolvelib import ( AbstractProvider, BaseReporter, @@ -21,9 +22,9 @@ ) from resolvelib.resolvers import ( Criterion, - Resolution, RequirementInformation, RequirementsConflicted, + Resolution, ) @@ -165,8 +166,8 @@ def run_resolver(*args): def test_pin_conflict_with_self(monkeypatch, reporter) -> None: """ - Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) - version for that same candidate (#91). + Verify correct behavior of attempting to pin a candidate version that conflicts + with a previously pinned (now invalidated) version for that same candidate (#91). """ Candidate = Tuple[ str, Version, Sequence[str] From a25f3bc97849cea8907a7d20fb2b67cd55bf12da Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 23:01:36 +0200 Subject: [PATCH 12/22] lint --- setup.cfg | 1 + tests/test_resolvers.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 238beba..ca15104 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ lint = mypy isort types-requests + types-setuptools test = commentjson packaging diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index cbadfe7..ad78764 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -20,11 +20,11 @@ ResolutionImpossible, Resolver, ) +from resolvelib.resolvers import Resolution # type: ignore from resolvelib.resolvers import ( Criterion, RequirementInformation, RequirementsConflicted, - Resolution, ) @@ -246,7 +246,7 @@ def get_updated_criterion_patch(self, candidate) -> None: result = resolver.resolve(["child", "parent"]) def get_child_versions( - information: Collection[RequirementInformation[str, Candidate]] + information: "Collection[RequirementInformation[str, Candidate]]", ) -> Set[str]: return { str(inf.parent[1]) From 85c9ce9c186500ef49ba2809745aba0a4139f030 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sat, 8 Oct 2022 23:35:22 +0200 Subject: [PATCH 13/22] old python compatibility --- src/resolvelib/resolvers.py | 2 +- tests/test_resolvers.py | 60 +++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 4faa6d4..ac80a4c 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -396,7 +396,7 @@ def resolve(self, requirements, max_rounds): return self.state # keep track of satisfied names to calculate diff ater pinning - satisfied_names = self.state.criteria.keys() - set( + satisfied_names = set(self.state.criteria.keys()) - set( unsatisfied_names ) diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index ad78764..6a80e62 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -1,5 +1,6 @@ from typing import ( - Collection, + Any, + Iterable, Iterator, List, Mapping, @@ -164,7 +165,8 @@ def run_resolver(*args): assert exception_causes == backtracking_causes -def test_pin_conflict_with_self(monkeypatch, reporter) -> None: +def test_pin_conflict_with_self(monkeypatch, reporter): + # type: (Any, BaseReporter) -> None """ Verify correct behavior of attempting to pin a candidate version that conflicts with a previously pinned (now invalidated) version for that same candidate (#91). @@ -172,7 +174,7 @@ def test_pin_conflict_with_self(monkeypatch, reporter) -> None: Candidate = Tuple[ str, Version, Sequence[str] ] # name, version, requirements - all_candidates: Mapping[str, Sequence[Candidate]] = { + all_candidates = { "parent": [("parent", Version("1"), ["child<2"])], "child": [ ("child", Version("2"), ["grandchild>=2"]), @@ -183,13 +185,12 @@ def test_pin_conflict_with_self(monkeypatch, reporter) -> None: ("grandchild", Version("2"), []), ("grandchild", Version("1"), []), ], - } + } # type: Mapping[str, Sequence[Candidate]] - class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] - def identify( - self, requirement_or_candidate: Union[str, Candidate] - ) -> str: - result: str = ( + class Provider(AbstractProvider): # AbstractProvider[int, Candidate, str] + def identify(self, requirement_or_candidate): + # type: (Union[str, Candidate]) -> str + result = ( Requirement.parse(requirement_or_candidate).key if isinstance(requirement_or_candidate, str) else requirement_or_candidate[0] @@ -197,21 +198,22 @@ def identify( assert result in all_candidates, "unknown requirement_or_candidate" return result - def get_preference( - self, identifier: str, *args: object, **kwargs: object - ) -> str: + def get_preference(self, identifier, *args, **kwargs): + # type: (str, *object, **object) -> str # prefer child over parent (alphabetically) return identifier - def get_dependencies(self, candidate: Candidate) -> Sequence[str]: + def get_dependencies(self, candidate): + # type: (Candidate) -> Sequence[str] return candidate[2] def find_matches( self, - identifier: str, - requirements: Mapping[str, Iterator[str]], - incompatibilities: Mapping[str, Iterator[Candidate]], - ) -> Iterator[Candidate]: + identifier, # type: str + requirements, # type: Mapping[str, Iterator[str]] + incompatibilities, # type: Mapping[str, Iterator[Candidate]] + ): + # type: (...) -> Iterator[Candidate] return ( candidate for candidate in all_candidates[identifier] @@ -222,32 +224,32 @@ def find_matches( if candidate not in incompatibilities[identifier] ) - def is_satisfied_by( - self, requirement: str, candidate: Candidate - ) -> bool: + def is_satisfied_by(self, requirement, candidate): + # type: (str, Candidate) -> bool return str(candidate[1]) in Requirement.parse(requirement) # patch Resolution._get_updated_criteria to collect rejected states - rejected_criteria: List[Criterion] = [] - get_updated_criterion_orig = Resolution._get_updated_criteria + rejected_criteria = [] # type: List[Criterion] + get_updated_criteria_orig = Resolution._get_updated_criteria - def get_updated_criterion_patch(self, candidate) -> None: + def get_updated_criteria_patch(self, candidate): try: - return get_updated_criterion_orig(self, candidate) + return get_updated_criteria_orig(self, candidate) except RequirementsConflicted as e: rejected_criteria.append(e.criterion) raise monkeypatch.setattr( - Resolution, "_get_updated_criteria", get_updated_criterion_patch + Resolution, "_get_updated_criteria", get_updated_criteria_patch ) - resolver: Resolver = Resolver(Provider(), reporter) + resolver = Resolver( + Provider(), reporter + ) # type: Resolver[str, Candidate, str] result = resolver.resolve(["child", "parent"]) - def get_child_versions( - information: "Collection[RequirementInformation[str, Candidate]]", - ) -> Set[str]: + def get_child_versions(information): + # type: (Iterable[RequirementInformation[str, Candidate]]) -> Set[str] return { str(inf.parent[1]) for inf in information From 65fa410bbfc007ccebb2b63a4015df481b59bd1f Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Wed, 12 Oct 2022 22:22:33 +0200 Subject: [PATCH 14/22] nitpicks --- src/resolvelib/resolvers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index ac80a4c..9ae8db4 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -201,7 +201,6 @@ def _remove_information_from_criteria(self, criteria, parents): ) def _get_preference(self, name): - # TODO: empty informations bug: verify + test case return self._p.get_preference( identifier=name, resolutions=self.state.mapping, @@ -395,7 +394,7 @@ def resolve(self, requirements, max_rounds): self._r.ending(state=self.state) return self.state - # keep track of satisfied names to calculate diff ater pinning + # keep track of satisfied names to calculate diff after pinning satisfied_names = set(self.state.criteria.keys()) - set( unsatisfied_names ) From 70c956793e44d645e5c0e97af8bbab7cb92356f7 Mon Sep 17 00:00:00 2001 From: Sander Date: Thu, 13 Oct 2022 08:14:26 +0200 Subject: [PATCH 15/22] Update src/resolvelib/resolvers.py Co-authored-by: Tzu-ping Chung --- src/resolvelib/resolvers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 9ae8db4..ea53a5e 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -174,14 +174,13 @@ def _add_to_criteria(self, criteria, requirement, parent): criteria[identifier] = criterion def _remove_information_from_criteria(self, criteria, parents): - """ - Removes information from a set of parents from a criteria. Concretely, removes - from each criterion's `information` field all values that have one of `parents` - as provider of the requirement. + """Remove information from parents of criteria. + + Concretely, removes all values from each criterion's ``information`` + field that have one of ``parents`` as provider of the requirement. :param criteria: The criteria to update. - :param parents: The set of identifiers for which to remove information from all - criteria. + :param parents: Identifiers for which to remove information from all criteria. """ if not parents: return From 5e85621a1b1f5c937d8ee28341b96a93f758c37b Mon Sep 17 00:00:00 2001 From: Sander Date: Thu, 13 Oct 2022 09:32:48 +0200 Subject: [PATCH 16/22] Update src/resolvelib/resolvers.py Co-authored-by: Frost Ming --- src/resolvelib/resolvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index ea53a5e..ed6216e 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -420,7 +420,7 @@ def resolve(self, requirements, max_rounds): key for key, criterion in self.state.criteria.items() if key in satisfied_names - if not self._is_current_pin_satisfying(key, criterion) + and not self._is_current_pin_satisfying(key, criterion) } self._remove_information_from_criteria( self.state.criteria, newly_unsatisfied_names From 7e841f82b0e98ccd348fa2b86509d948ac23e3cd Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Thu, 13 Oct 2022 10:58:45 +0200 Subject: [PATCH 17/22] pep8 --- src/resolvelib/resolvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index ed6216e..b01170d 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -175,7 +175,7 @@ def _add_to_criteria(self, criteria, requirement, parent): def _remove_information_from_criteria(self, criteria, parents): """Remove information from parents of criteria. - + Concretely, removes all values from each criterion's ``information`` field that have one of ``parents`` as provider of the requirement. From cc1c220d0c15980fef81adaa6de336c40d6820a3 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Fri, 14 Oct 2022 18:07:00 +0200 Subject: [PATCH 18/22] added news fragment --- news/91.bugfix.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 news/91.bugfix.rst diff --git a/news/91.bugfix.rst b/news/91.bugfix.rst new file mode 100644 index 0000000..6b16158 --- /dev/null +++ b/news/91.bugfix.rst @@ -0,0 +1,6 @@ +Some valid states that were previously rejected are now accepted. This affects +states where multiple candidates for the same dependency conflict with each +other. The ``information`` argument passed to +``AbstractProvider.get_preference`` may now contain empty iterators. This has +always been allowed by the method definition but it was previously not possible +in practice. From ad9eaca8a0c2f9ccff8c04186be6cff1e1d547dd Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Fri, 14 Oct 2022 18:09:15 +0200 Subject: [PATCH 19/22] removed TODO --- src/resolvelib/resolvers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index b01170d..49e30c7 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -187,7 +187,6 @@ def _remove_information_from_criteria(self, criteria, parents): for key, criterion in criteria.items(): criteria[key] = Criterion( criterion.candidates, - # TODO: is empty information allowed? [ information for information in criterion.information From 20df2c8ba8a6905b5166c931a0ae820969698894 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Wed, 16 Nov 2022 20:08:05 +0100 Subject: [PATCH 20/22] use packaging instead of setuptools --- setup.cfg | 1 - tests/test_resolvers.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index ca15104..238beba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,6 @@ lint = mypy isort types-requests - types-setuptools test = commentjson packaging diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 6a80e62..4304f5a 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -12,7 +12,7 @@ import pytest from packaging.version import Version -from pkg_resources import Requirement +from packaging.requirements import Requirement from resolvelib import ( AbstractProvider, @@ -191,7 +191,7 @@ class Provider(AbstractProvider): # AbstractProvider[int, Candidate, str] def identify(self, requirement_or_candidate): # type: (Union[str, Candidate]) -> str result = ( - Requirement.parse(requirement_or_candidate).key + Requirement(requirement_or_candidate).name if isinstance(requirement_or_candidate, str) else requirement_or_candidate[0] ) @@ -226,7 +226,7 @@ def find_matches( def is_satisfied_by(self, requirement, candidate): # type: (str, Candidate) -> bool - return str(candidate[1]) in Requirement.parse(requirement) + return candidate[1] in Requirement(requirement).specifier # patch Resolution._get_updated_criteria to collect rejected states rejected_criteria = [] # type: List[Criterion] From 6e9cf49c3d373fde265dddcc3a51dbda43d8912d Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Wed, 16 Nov 2022 20:16:16 +0100 Subject: [PATCH 21/22] pep8 --- tests/test_resolvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 4304f5a..01159e9 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -11,8 +11,8 @@ ) import pytest -from packaging.version import Version from packaging.requirements import Requirement +from packaging.version import Version from resolvelib import ( AbstractProvider, From 546a11f6f8d00e3b0ea71a1862bae7e8816ebdbe Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Wed, 16 Nov 2022 20:51:31 +0100 Subject: [PATCH 22/22] added Resolution to type stub --- src/resolvelib/resolvers.pyi | 12 ++++++++++++ tests/test_resolvers.py | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/resolvelib/resolvers.pyi b/src/resolvelib/resolvers.pyi index 0eb5b21..528a1a2 100644 --- a/src/resolvelib/resolvers.pyi +++ b/src/resolvelib/resolvers.pyi @@ -55,6 +55,18 @@ class ResolutionImpossible(ResolutionError, Generic[RT, CT]): class ResolutionTooDeep(ResolutionError): round_count: int +# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. +# https://stackoverflow.com/a/50531189/1376863 +class State(tuple, Generic[RT, CT, KT]): + mapping: Mapping[KT, CT] + criteria: Mapping[KT, Criterion[RT, CT, KT]] + backtrack_causes: Collection[RequirementInformation[RT, CT]] + +class Resolution(Generic[RT, CT, KT]): + def resolve( + self, requirements: Iterable[RT], max_rounds: int + ) -> State[RT, CT, KT]: ... + class Result(Generic[RT, CT, KT]): mapping: Mapping[KT, CT] graph: DirectedGraph[Optional[KT]] diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 01159e9..176108f 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -21,11 +21,11 @@ ResolutionImpossible, Resolver, ) -from resolvelib.resolvers import Resolution # type: ignore from resolvelib.resolvers import ( Criterion, RequirementInformation, RequirementsConflicted, + Resolution, ) @@ -187,7 +187,7 @@ def test_pin_conflict_with_self(monkeypatch, reporter): ], } # type: Mapping[str, Sequence[Candidate]] - class Provider(AbstractProvider): # AbstractProvider[int, Candidate, str] + class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str] def identify(self, requirement_or_candidate): # type: (Union[str, Candidate]) -> str result = ( @@ -230,7 +230,9 @@ def is_satisfied_by(self, requirement, candidate): # patch Resolution._get_updated_criteria to collect rejected states rejected_criteria = [] # type: List[Criterion] - get_updated_criteria_orig = Resolution._get_updated_criteria + get_updated_criteria_orig = ( + Resolution._get_updated_criteria # type: ignore[attr-defined] + ) def get_updated_criteria_patch(self, candidate): try: