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-07 17:34:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-resolvelib (Old)
 and      /work/SRC/openSUSE:Factory/.python-resolvelib.new.1835 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-resolvelib"

Wed Dec  7 17:34:53 2022 rev:9 rq:1040739 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-resolvelib/python-resolvelib.changes      
2022-12-04 16:17:07.375487179 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-resolvelib.new.1835/python-resolvelib.changes
    2022-12-07 17:36:12.808949293 +0100
@@ -1,0 +2,15 @@
+Tue Dec  6 15:28:03 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com>
+
+- Update to version 0.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.WQytpG/_old  2022-12-07 17:36:13.252951724 +0100
+++ /var/tmp/diff_new_pack.WQytpG/_new  2022-12-07 17:36:13.256951746 +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 = uranu...@gmail.com
 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")

Reply via email to