Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-resolvelib for
openSUSE:Factory checked in at 2022-12-04 14:58:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-resolvelib (Old)
and /work/SRC/openSUSE:Factory/.python-resolvelib.new.1835 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-resolvelib"
Sun Dec 4 14:58:46 2022 rev:6 rq:1039901 version:0.9.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-resolvelib/python-resolvelib.changes
2022-07-19 17:19:11.628353195 +0200
+++
/work/SRC/openSUSE:Factory/.python-resolvelib.new.1835/python-resolvelib.changes
2022-12-04 14:59:15.736584108 +0100
@@ -1,0 +2,12 @@
+Sat Dec 3 21:52:31 UTC 2022 - Yogalakshmi Arunachalam <[email protected]>
+
+- Update to v0.9.0
+ Features
+ * A new reporter hook rejecting_candidate is added, replacing backtracking.
The hook is called every time the resolver
+ rejects a conflicting candidate before trying out the next one in line.
#101
+ Bug Fixes
+ * 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. #91
+
+-------------------------------------------------------------------
Old:
----
resolvelib-0.8.1.tar.gz
New:
----
resolvelib-0.9.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-resolvelib.spec ++++++
--- /var/tmp/diff_new_pack.1f2LAq/_old 2022-12-04 14:59:16.696589639 +0100
+++ /var/tmp/diff_new_pack.1f2LAq/_new 2022-12-04 14:59:16.704589685 +0100
@@ -18,7 +18,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-resolvelib
-Version: 0.8.1
+Version: 0.9.0
Release: 0
Summary: Module to resolve abstract dependencies into concrete ones
License: ISC
++++++ resolvelib-0.8.1.tar.gz -> resolvelib-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/CHANGELOG.rst
new/resolvelib-0.9.0/CHANGELOG.rst
--- old/resolvelib-0.8.1/CHANGELOG.rst 2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/CHANGELOG.rst 2022-11-16 21:11:34.000000000 +0100
@@ -1,3 +1,25 @@
+0.9.0 (2022-11-17)
+==================
+
+Features
+--------
+
+- A new reporter hook ``rejecting_candidate`` is added, replacing
``backtracking``.
+ The hook is called every time the resolver rejects a conflicting candidate
before
+ trying out the next one in line. `#101
<https://github.com/sarugaku/resolvelib/issues/101>`_
+
+
+Bug Fixes
+---------
+
+- 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. `#91 <https://github.com/sarugaku/resolvelib/issues/91>`_
+
+
0.8.1 (2021-10-12)
==================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/README.rst
new/resolvelib-0.9.0/README.rst
--- old/resolvelib-0.8.1/README.rst 2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/README.rst 2022-11-16 21:11:34.000000000 +0100
@@ -56,8 +56,8 @@
-------
A string, usually in a number form, describing a snapshot of a Package. This
-number should increase when a Package post a new snapshot, i.e. a higher number
-means a more up-to-date snapshot.
+number should increase when a Package posts a new snapshot,
+i.e a higher number means a more up-to-date snapshot.
Specifier
---------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/noxfile.py
new/resolvelib-0.9.0/noxfile.py
--- old/resolvelib-0.8.1/noxfile.py 2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/noxfile.py 2022-11-16 21:11:34.000000000 +0100
@@ -82,7 +82,7 @@
if options.version:
_write_package_version(options.version)
- session.run("towncrier", "--version", options.version)
+ session.run("towncrier", "build", "--version", options.version)
session.run(
"git",
"commit",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/setup.cfg
new/resolvelib-0.9.0/setup.cfg
--- old/resolvelib-0.8.1/setup.cfg 2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/setup.cfg 2022-11-16 21:11:34.000000000 +0100
@@ -6,6 +6,7 @@
author = Tzu-ping Chung
author_email = [email protected]
long_description = file: README.rst
+long_description_content_type = text/x-rst
license = ISC License
keywords =
dependency
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/__init__.py
new/resolvelib-0.9.0/src/resolvelib/__init__.py
--- old/resolvelib-0.8.1/src/resolvelib/__init__.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/__init__.py 2022-11-16
21:11:34.000000000 +0100
@@ -11,7 +11,7 @@
"ResolutionTooDeep",
]
-__version__ = "0.8.1"
+__version__ = "0.9.0"
from .providers import AbstractProvider, AbstractResolver
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/resolvelib-0.8.1/src/resolvelib/compat/collections_abc.pyi
new/resolvelib-0.9.0/src/resolvelib/compat/collections_abc.pyi
--- old/resolvelib-0.8.1/src/resolvelib/compat/collections_abc.pyi
1970-01-01 01:00:00.000000000 +0100
+++ new/resolvelib-0.9.0/src/resolvelib/compat/collections_abc.pyi
2022-11-16 21:11:34.000000000 +0100
@@ -0,0 +1 @@
+from collections.abc import Mapping, Sequence
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/providers.py
new/resolvelib-0.9.0/src/resolvelib/providers.py
--- old/resolvelib-0.8.1/src/resolvelib/providers.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/providers.py 2022-11-16
21:11:34.000000000 +0100
@@ -1,5 +1,5 @@
class AbstractProvider(object):
- """Delegate class to provide requirement interface for the resolver."""
+ """Delegate class to provide the required interface for the resolver."""
def identify(self, requirement_or_candidate):
"""Given a requirement, return an identifier for it.
@@ -24,9 +24,9 @@
this group of arguments is.
:param identifier: An identifier as returned by ``identify()``. This
- identifies the dependency matches of which should be returned.
+ identifies the dependency matches which should be returned.
:param resolutions: Mapping of candidates currently pinned by the
- resolver. Each key is an identifier, and the value a candidate.
+ resolver. Each key is an identifier, and the value is a candidate.
The candidate may conflict with requirements from ``information``.
:param candidates: Mapping of each dependency's possible candidates.
Each value is an iterator of candidates.
@@ -39,10 +39,10 @@
* ``requirement`` specifies a requirement contributing to the current
list of candidates.
- * ``parent`` specifies the candidate that provides (dependend on) the
+ * ``parent`` specifies the candidate that provides (depended on) the
requirement, or ``None`` to indicate a root requirement.
- The preference could depend on a various of issues, including (not
+ The preference could depend on various issues, including (not
necessarily in this order):
* Is this package pinned in the current resolution result?
@@ -61,7 +61,7 @@
raise NotImplementedError
def find_matches(self, identifier, requirements, incompatibilities):
- """Find all possible candidates that satisfy given constraints.
+ """Find all possible candidates that satisfy the given constraints.
:param identifier: An identifier as returned by ``identify()``. This
identifies the dependency matches of which should be returned.
@@ -92,7 +92,7 @@
def is_satisfied_by(self, requirement, candidate):
"""Whether the given requirement can be satisfied by a candidate.
- The candidate is guarenteed to have been generated from the
+ The candidate is guaranteed to have been generated from the
requirement.
A boolean should be returned to indicate whether ``candidate`` is a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/providers.pyi
new/resolvelib-0.9.0/src/resolvelib/providers.pyi
--- old/resolvelib-0.8.1/src/resolvelib/providers.pyi 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/providers.pyi 2022-11-16
21:11:34.000000000 +0100
@@ -1,12 +1,11 @@
from typing import (
Any,
- Collection,
Generic,
Iterable,
Iterator,
Mapping,
- Optional,
Protocol,
+ Sequence,
Union,
)
@@ -25,6 +24,7 @@
resolutions: Mapping[KT, CT],
candidates: Mapping[KT, Iterator[CT]],
information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]],
+ backtrack_causes: Sequence[RequirementInformation[RT, CT]],
) -> Preference: ...
def find_matches(
self,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/reporters.py
new/resolvelib-0.9.0/src/resolvelib/reporters.py
--- old/resolvelib-0.8.1/src/resolvelib/reporters.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/reporters.py 2022-11-16
21:11:34.000000000 +0100
@@ -36,7 +36,7 @@
:param causes: The information on the collision that caused the
backtracking.
"""
- def backtracking(self, candidate):
+ def rejecting_candidate(self, criterion, candidate):
"""Called when rejecting a candidate during backtracking."""
def pinning(self, candidate):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/reporters.pyi
new/resolvelib-0.9.0/src/resolvelib/reporters.pyi
--- old/resolvelib-0.8.1/src/resolvelib/reporters.pyi 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/reporters.pyi 2022-11-16
21:11:34.000000000 +0100
@@ -6,6 +6,6 @@
def ending_round(self, index: int, state: Any) -> Any: ...
def ending(self, state: Any) -> Any: ...
def adding_requirement(self, requirement: Any, parent: Any) -> Any: ...
- def backtracking(self, candidate: Any) -> Any: ...
+ def rejecting_candidate(self, criterion: Any, candidate: Any) -> Any: ...
def resolving_conflicts(self, causes: Any) -> Any: ...
def pinning(self, candidate: Any) -> Any: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/resolvers.py
new/resolvelib-0.9.0/src/resolvelib/resolvers.py
--- old/resolvelib-0.8.1/src/resolvelib/resolvers.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/resolvers.py 2022-11-16
21:11:34.000000000 +0100
@@ -173,6 +173,31 @@
raise RequirementsConflicted(criterion)
criteria[identifier] = criterion
+ 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.
+
+ :param criteria: The criteria to update.
+ :param parents: Identifiers for which to remove information from all
criteria.
+ """
+ if not parents:
+ return
+ for key, criterion in criteria.items():
+ criteria[key] = Criterion(
+ criterion.candidates,
+ [
+ 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):
return self._p.get_preference(
identifier=name,
@@ -212,6 +237,7 @@
try:
criteria = self._get_updated_criteria(candidate)
except RequirementsConflicted as e:
+ self._r.rejecting_candidate(e.criterion, candidate)
causes.append(e.criterion)
continue
@@ -281,8 +307,6 @@
# Also mark the newly known incompatibility.
incompatibilities_from_broken.append((name, [candidate]))
- self._r.backtracking(candidate=candidate)
-
# Create a new state from the last known-to-work one, and apply
# the previously gathered incompatibility information.
def _patch_criteria():
@@ -368,6 +392,11 @@
self._r.ending(state=self.state)
return self.state
+ # keep track of satisfied names to calculate diff after pinning
+ satisfied_names = set(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)
@@ -384,6 +413,17 @@
if not success:
raise ResolutionImpossible(self.state.backtrack_causes)
else:
+ # 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
+ and not self._is_current_pin_satisfying(key, criterion)
+ }
+ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/resolvers.pyi
new/resolvelib-0.9.0/src/resolvelib/resolvers.pyi
--- old/resolvelib-0.8.1/src/resolvelib/resolvers.pyi 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/resolvers.pyi 2022-11-16
21:11:34.000000000 +0100
@@ -55,6 +55,18 @@
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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/structs.py
new/resolvelib-0.9.0/src/resolvelib/structs.py
--- old/resolvelib-0.8.1/src/resolvelib/structs.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/structs.py 2022-11-16
21:11:34.000000000 +0100
@@ -117,13 +117,14 @@
def __init__(self, factory):
self._factory = factory
+ self._iterable = None
def __repr__(self):
- return "{}({})".format(type(self).__name__, list(self._factory()))
+ return "{}({})".format(type(self).__name__, list(self))
def __bool__(self):
try:
- next(self._factory())
+ next(iter(self))
except StopIteration:
return False
return True
@@ -131,7 +132,11 @@
__nonzero__ = __bool__ # XXX: Python 2.
def __iter__(self):
- return self._factory()
+ iterable = (
+ self._factory() if self._iterable is None else self._iterable
+ )
+ self._iterable, current = itertools.tee(iterable)
+ return current
class _SequenceIterableView(object):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/structs.pyi
new/resolvelib-0.9.0/src/resolvelib/structs.pyi
--- old/resolvelib-0.8.1/src/resolvelib/structs.pyi 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/src/resolvelib/structs.pyi 2022-11-16
21:11:34.000000000 +0100
@@ -16,7 +16,7 @@
CT = TypeVar("CT") # Candidate.
_T = TypeVar("_T")
-Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]]
+Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]]
class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta):
pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/tests/conftest.py
new/resolvelib-0.9.0/tests/conftest.py
--- old/resolvelib-0.8.1/tests/conftest.py 2021-10-11 23:05:57.000000000
+0200
+++ new/resolvelib-0.9.0/tests/conftest.py 2022-11-16 21:11:34.000000000
+0100
@@ -9,10 +9,9 @@
def __init__(self):
self._indent = 0
- def backtracking(self, candidate):
+ def rejecting_candidate(self, criterion, candidate):
self._indent -= 1
- assert self._indent >= 0
- print(" " * self._indent, "Back ", candidate, sep="")
+ print(" " * self._indent, "Reject ", candidate, sep="")
def pinning(self, candidate):
print(" " * self._indent, "Pin ", candidate, sep="")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/resolvelib-0.8.1/tests/functional/python/inputs/index/same-package.json
new/resolvelib-0.9.0/tests/functional/python/inputs/index/same-package.json
--- old/resolvelib-0.8.1/tests/functional/python/inputs/index/same-package.json
2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/tests/functional/python/inputs/index/same-package.json
2022-11-16 21:11:34.000000000 +0100
@@ -2,37 +2,37 @@
"package-a": {
"0.1.0": {
"dependencies": [
- "package-x=='0.1.0'; extra == 'x'",
- "package-y=='0.1.0'; extra == 'y'",
- "package-z=='0.1.0'; extra == 'z'"
+ "package-x==0.1.0; extra == 'x'",
+ "package-y==0.1.0; extra == 'y'",
+ "package-z==0.1.0; extra == 'z'"
]
},
"1.0.0": {
"dependencies": [
- "package-x=='1.0.0'; extra == 'x'",
- "package-y=='1.0.0'; extra == 'y'",
- "package-z=='1.0.0'; extra == 'z'"
+ "package-x==1.0.0; extra == 'x'",
+ "package-y==1.0.0; extra == 'y'",
+ "package-z==1.0.0; extra == 'z'"
]
},
"1.1.0": {
"dependencies": [
- "package-x=='1.1.0'; extra == 'x'",
- "package-y=='1.1.0'; extra == 'y'",
- "package-z=='1.1.0'; extra == 'z'"
+ "package-x==1.1.0; extra == 'x'",
+ "package-y==1.1.0; extra == 'y'",
+ "package-z==1.1.0; extra == 'z'"
]
},
"1.2.0": {
"dependencies": [
- "package-x=='1.2.0'; extra == 'x'",
- "package-y=='1.2.0'; extra == 'y'",
- "package-z=='1.2.0'; extra == 'z'"
+ "package-x==1.2.0; extra == 'x'",
+ "package-y==1.2.0; extra == 'y'",
+ "package-z==1.2.0; extra == 'z'"
]
},
"1.3.0": {
"dependencies": [
- "package-x=='1.3.0'; extra == 'x'",
- "package-y=='1.3.0'; extra == 'y'",
- "package-z=='1.3.0'; extra == 'z'"
+ "package-x==1.3.0; extra == 'x'",
+ "package-y==1.3.0; extra == 'y'",
+ "package-z==1.3.0; extra == 'z'"
]
}
},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/resolvelib-0.8.1/tests/functional/python/test_resolvers_python.py
new/resolvelib-0.9.0/tests/functional/python/test_resolvers_python.py
--- old/resolvelib-0.8.1/tests/functional/python/test_resolvers_python.py
2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/tests/functional/python/test_resolvers_python.py
2022-11-16 21:11:34.000000000 +0100
@@ -129,7 +129,6 @@
XFAIL_CASES = {
"pyrex-1.9.8.json": "Too many rounds (>500)",
- "same-package-extras.json": "State not cleaned up correctly",
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/resolvelib-0.8.1/tests/test_resolvers.py
new/resolvelib-0.9.0/tests/test_resolvers.py
--- old/resolvelib-0.8.1/tests/test_resolvers.py 2021-10-11
23:05:57.000000000 +0200
+++ new/resolvelib-0.9.0/tests/test_resolvers.py 2022-11-16
21:11:34.000000000 +0100
@@ -1,4 +1,18 @@
+from typing import (
+ Any,
+ Iterable,
+ Iterator,
+ List,
+ Mapping,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+)
+
import pytest
+from packaging.requirements import Requirement
+from packaging.version import Version
from resolvelib import (
AbstractProvider,
@@ -7,6 +21,12 @@
ResolutionImpossible,
Resolver,
)
+from resolvelib.resolvers import (
+ Criterion,
+ RequirementInformation,
+ RequirementsConflicted,
+ Resolution,
+)
def test_candidate_inconsistent_error():
@@ -143,3 +163,109 @@
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):
+ # 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).
+ """
+ Candidate = Tuple[
+ str, Version, Sequence[str]
+ ] # name, version, requirements
+ all_candidates = {
+ "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"), []),
+ ],
+ } # type: Mapping[str, Sequence[Candidate]]
+
+ class Provider(AbstractProvider): # AbstractProvider[str, Candidate, str]
+ def identify(self, requirement_or_candidate):
+ # type: (Union[str, Candidate]) -> str
+ result = (
+ Requirement(requirement_or_candidate).name
+ 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, *args, **kwargs):
+ # type: (str, *object, **object) -> str
+ # prefer child over parent (alphabetically)
+ return identifier
+
+ def get_dependencies(self, candidate):
+ # type: (Candidate) -> Sequence[str]
+ return candidate[2]
+
+ def find_matches(
+ self,
+ 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]
+ 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, candidate):
+ # type: (str, Candidate) -> bool
+ return candidate[1] in Requirement(requirement).specifier
+
+ # patch Resolution._get_updated_criteria to collect rejected states
+ rejected_criteria = [] # type: List[Criterion]
+ get_updated_criteria_orig = (
+ Resolution._get_updated_criteria # type: ignore[attr-defined]
+ )
+
+ def get_updated_criteria_patch(self, candidate):
+ try:
+ 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_criteria_patch
+ )
+
+ resolver = Resolver(
+ Provider(), reporter
+ ) # type: Resolver[str, Candidate, str]
+ result = resolver.resolve(["child", "parent"])
+
+ def get_child_versions(information):
+ # type: (Iterable[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")