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 2023-12-08 22:33:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-resolvelib (Old)
 and      /work/SRC/openSUSE:Factory/.python-resolvelib.new.25432 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-resolvelib"

Fri Dec  8 22:33:33 2023 rev:14 rq:1132100 version:1.0.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-resolvelib/python-resolvelib.changes      
2023-05-08 17:24:38.668823112 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-resolvelib.new.25432/python-resolvelib.changes
   2023-12-08 22:34:26.117258251 +0100
@@ -1,0 +2,21 @@
+Fri Dec  8 13:39:00 UTC 2023 - Dirk Müller <dmuel...@suse.com>
+
+- update to 1.0.1:
+  * Fix calls to opaque objects and use provider interface calls
+    instead.  `#126
+  * Implement backjumping to significantly speed up the
+    resolution process by skipping over irrelevant parts of the
+    resolution search space.  `#113
+  * 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.
+  * 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.
+
+-------------------------------------------------------------------

Old:
----
  resolvelib-0.8.1.tar.gz

New:
----
  resolvelib-1.0.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-resolvelib.spec ++++++
--- /var/tmp/diff_new_pack.8MFJkz/_old  2023-12-08 22:34:26.617276649 +0100
+++ /var/tmp/diff_new_pack.8MFJkz/_new  2023-12-08 22:34:26.617276649 +0100
@@ -19,7 +19,7 @@
 %{?sle15_python_module_pythons}
 Name:           python-resolvelib
 # ansible-core 2.14.x is currently requiring < 0.9.0
-Version:        0.8.1
+Version:        1.0.1
 Release:        0
 Summary:        Module to resolve abstract dependencies into concrete ones
 License:        ISC

++++++ resolvelib-0.8.1.tar.gz -> resolvelib-1.0.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/resolvelib-0.8.1/.github/workflows/ci.yml 
new/resolvelib-1.0.1/.github/workflows/ci.yml
--- old/resolvelib-0.8.1/.github/workflows/ci.yml       2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/.github/workflows/ci.yml       2023-03-09 
06:10:19.000000000 +0100
@@ -9,39 +9,35 @@
     name: Lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-      - run: pip install .[lint,test]
-      - run: black --check .
-      - run: isort .
-      - run: flake8 .
-      - run: mypy src/ tests/
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+      - run: pip install nox
+      - run: nox -s lint
   package:
     name: Package
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-      - run: pip install .[release]
-      - run: python -m build .
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+      - run: pip install nox
+      - run: nox -s release -- --version '' --repo '' --prebump ''
   test:
     name: Test
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     needs: [lint]
     strategy:
       fail-fast: true
       matrix:
         python:
-          - "2.7"
+          - "3.11"
           - "3.10"
           - "3.9"
           - "3.8"
           - "3.7"
-          - "3.6"
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python }}
-      - run: pip install .[test]
-      - run: pytest tests
+      - run: pip install nox
+      - run: nox -s tests-${{ matrix.python }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/resolvelib-0.8.1/CHANGELOG.rst 
new/resolvelib-1.0.1/CHANGELOG.rst
--- old/resolvelib-0.8.1/CHANGELOG.rst  2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/CHANGELOG.rst  2023-03-09 06:10:19.000000000 +0100
@@ -1,3 +1,43 @@
+1.0.1 (2023-03-09)
+==================
+
+Bug Fixes
+---------
+
+- Fix calls to opaque objects and use provider interface calls instead.  `#126 
<https://github.com/sarugaku/resolvelib/issues/126>`_
+
+
+1.0.0 (2023-03-08)
+==================
+
+Features
+--------
+
+- Implement backjumping to significantly speed up the resolution process by 
skipping over irrelevant parts of the resolution search space.  `#113 
<https://github.com/sarugaku/resolvelib/issues/113>`_
+
+
+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/CODE_OF_CONDUCT.md 
new/resolvelib-1.0.1/CODE_OF_CONDUCT.md
--- old/resolvelib-0.8.1/CODE_OF_CONDUCT.md     1970-01-01 01:00:00.000000000 
+0100
+++ new/resolvelib-1.0.1/CODE_OF_CONDUCT.md     2023-03-09 06:10:19.000000000 
+0100
@@ -0,0 +1 @@
+Everyone interacting in the ResolveLib project's codebases and issue trackers 
is expected to follow the [PSF Code of 
Conduct](https://www.python.org/psf/conduct/).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/resolvelib-0.8.1/DEVELOPMENT.rst 
new/resolvelib-1.0.1/DEVELOPMENT.rst
--- old/resolvelib-0.8.1/DEVELOPMENT.rst        2021-10-11 23:05:57.000000000 
+0200
+++ new/resolvelib-1.0.1/DEVELOPMENT.rst        2023-03-09 06:10:19.000000000 
+0100
@@ -32,10 +32,14 @@
 
 Replace ``X.Y.Z`` with the release you would like to make.
 
+(The following assumes the remote you forked is called ``origin``, and the 
canonical sarugaku/resolvelib is under ``upstream``.)
+
 * Make sure the news fragments are in place.
+* ``git checkout -b release/X.Y.Z``
 * ``nox -s release -- --repo https://upload.pypi.org/legacy/ --prebump 
X.Y.Z+1.dev0 --version X.Y.Z``
-* ``git push origin master --tags``
-* ``git push upstream master --tags``
+* ``git push upstream --tags``
+* ``git push origin release/X.Y.Z``
+* Open a pull request on GitHub and merge the ``release/X.Y.Z`` branch into 
main.
 
 Breakdown of the ``release`` nox task:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/resolvelib-0.8.1/README.rst 
new/resolvelib-1.0.1/README.rst
--- old/resolvelib-0.8.1/README.rst     2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/README.rst     2023-03-09 06:10:19.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-1.0.1/noxfile.py
--- old/resolvelib-0.8.1/noxfile.py     2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/noxfile.py     2023-03-09 06:10:19.000000000 +0100
@@ -22,7 +22,7 @@
     session.run("mypy", "src", "tests")
 
 
-@nox.session(python=["3.9", "3.8", "3.7", "3.6", "3.5", "2.7"])
+@nox.session(python=["3.11", "3.10", "3.9", "3.8", "3.7", "2.7"])
 def tests(session):
     session.install(".[test]")
 
@@ -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-1.0.1/setup.cfg
--- old/resolvelib-0.8.1/setup.cfg      2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/setup.cfg      2023-03-09 06:10:19.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
@@ -56,7 +57,7 @@
 [flake8]
 max-line-length = 88
 select = C,E,F,W,B
-ignore = E203, W503
+ignore = E203, W503, F401
 exclude =
        .git,
        .venv,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/resolvelib-0.8.1/src/resolvelib/__init__.py 
new/resolvelib-1.0.1/src/resolvelib/__init__.py
--- old/resolvelib-0.8.1/src/resolvelib/__init__.py     2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/__init__.py     2023-03-09 
06:10:19.000000000 +0100
@@ -11,7 +11,7 @@
     "ResolutionTooDeep",
 ]
 
-__version__ = "0.8.1"
+__version__ = "1.0.1"
 
 
 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-1.0.1/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-1.0.1/src/resolvelib/compat/collections_abc.pyi      
2023-03-09 06:10:19.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-1.0.1/src/resolvelib/providers.py
--- old/resolvelib-0.8.1/src/resolvelib/providers.py    2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/providers.py    2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/providers.pyi
--- old/resolvelib-0.8.1/src/resolvelib/providers.pyi   2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/providers.pyi   2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/reporters.py
--- old/resolvelib-0.8.1/src/resolvelib/reporters.py    2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/reporters.py    2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/reporters.pyi
--- old/resolvelib-0.8.1/src/resolvelib/reporters.pyi   2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/reporters.pyi   2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/resolvers.py
--- old/resolvelib-0.8.1/src/resolvelib/resolvers.py    2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/resolvers.py    2023-03-09 
06:10:19.000000000 +0100
@@ -1,4 +1,5 @@
 import collections
+import itertools
 import operator
 
 from .providers import AbstractResolver
@@ -173,6 +174,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.parent is None
+                        or self._p.identify(information.parent) not in parents
+                    )
+                ],
+                criterion.incompatibilities,
+            )
+
     def _get_preference(self, name):
         return self._p.get_preference(
             identifier=name,
@@ -212,6 +238,7 @@
             try:
                 criteria = self._get_updated_criteria(candidate)
             except RequirementsConflicted as e:
+                self._r.rejecting_candidate(e.criterion, candidate)
                 causes.append(e.criterion)
                 continue
 
@@ -240,8 +267,8 @@
         # end, signal for backtracking.
         return causes
 
-    def _backtrack(self):
-        """Perform backtracking.
+    def _backjump(self, causes):
+        """Perform backjumping.
 
         When we enter here, the stack is like this::
 
@@ -257,22 +284,46 @@
 
         Each iteration of the loop will:
 
-        1.  Discard Z.
-        2.  Discard Y but remember its incompatibility information gathered
+        1.  Identify Z. The incompatibility is not always caused by the latest
+            state. For example, given three requirements A, B and C, with
+            dependencies A1, B1 and C1, where A1 and B1 are incompatible: the
+            last state might be related to C, so we want to discard the
+            previous state.
+        2.  Discard Z.
+        3.  Discard Y but remember its incompatibility information gathered
             previously, and the failure we're dealing with right now.
-        3.  Push a new state Y' based on X, and apply the incompatibility
+        4.  Push a new state Y' based on X, and apply the incompatibility
             information from Y to Y'.
-        4a. If this causes Y' to conflict, we need to backtrack again. Make Y'
+        5a. If this causes Y' to conflict, we need to backtrack again. Make Y'
             the new Z and go back to step 2.
-        4b. If the incompatibilities apply cleanly, end backtracking.
+        5b. If the incompatibilities apply cleanly, end backtracking.
         """
+        incompatible_reqs = itertools.chain(
+            (c.parent for c in causes if c.parent is not None),
+            (c.requirement for c in causes),
+        )
+        incompatible_deps = {self._p.identify(r) for r in incompatible_reqs}
         while len(self._states) >= 3:
             # Remove the state that triggered backtracking.
             del self._states[-1]
 
-            # Retrieve the last candidate pin and known incompatibilities.
-            broken_state = self._states.pop()
-            name, candidate = broken_state.mapping.popitem()
+            # Ensure to backtrack to a state that caused the incompatibility
+            incompatible_state = False
+            while not incompatible_state:
+                # Retrieve the last candidate pin and known incompatibilities.
+                try:
+                    broken_state = self._states.pop()
+                    name, candidate = broken_state.mapping.popitem()
+                except (IndexError, KeyError):
+                    raise ResolutionImpossible(causes)
+                current_dependencies = {
+                    self._p.identify(d)
+                    for d in self._p.get_dependencies(candidate)
+                }
+                incompatible_state = not current_dependencies.isdisjoint(
+                    incompatible_deps
+                )
+
             incompatibilities_from_broken = [
                 (k, list(v.incompatibilities))
                 for k, v in broken_state.criteria.items()
@@ -281,8 +332,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,22 +417,38 @@
                 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)
 
             if failure_causes:
                 causes = [i for c in failure_causes for i in c.information]
-                # Backtrack if pinning fails. The backtrack process puts us in
+                # Backjump if pinning fails. The backjump process puts us in
                 # an unpinned state, so we can work on it in the next round.
                 self._r.resolving_conflicts(causes=causes)
-                success = self._backtrack()
+                success = self._backjump(causes)
                 self.state.backtrack_causes[:] = causes
 
                 # Dead ends everywhere. Give up.
                 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-1.0.1/src/resolvelib/resolvers.pyi
--- old/resolvelib-0.8.1/src/resolvelib/resolvers.pyi   2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/resolvers.pyi   2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/structs.py
--- old/resolvelib-0.8.1/src/resolvelib/structs.py      2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/structs.py      2023-03-09 
06:10:19.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-1.0.1/src/resolvelib/structs.pyi
--- old/resolvelib-0.8.1/src/resolvelib/structs.pyi     2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/src/resolvelib/structs.pyi     2023-03-09 
06:10:19.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-1.0.1/tests/conftest.py
--- old/resolvelib-0.8.1/tests/conftest.py      2021-10-11 23:05:57.000000000 
+0200
+++ new/resolvelib-1.0.1/tests/conftest.py      2023-03-09 06:10:19.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/cocoapods/test_resolvers_cocoapods.py 
new/resolvelib-1.0.1/tests/functional/cocoapods/test_resolvers_cocoapods.py
--- old/resolvelib-0.8.1/tests/functional/cocoapods/test_resolvers_cocoapods.py 
2021-10-11 23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/tests/functional/cocoapods/test_resolvers_cocoapods.py 
2023-03-09 06:10:19.000000000 +0100
@@ -6,8 +6,6 @@
 import string
 
 import commentjson  # type: ignore
-import packaging.specifiers
-import packaging.version
 import pytest
 
 from resolvelib import AbstractProvider, ResolutionImpossible, Resolver
@@ -17,40 +15,112 @@
 
 
 INPUTS_DIR = os.path.abspath(os.path.join(__file__, "..", "inputs"))
-
 CASE_DIR = os.path.join(INPUTS_DIR, "case")
-
 CASE_NAMES = [name for name in os.listdir(CASE_DIR) if name.endswith(".json")]
 
 
-def _convert_specifier(s):
-    if not s:
-        return s
-    m = re.match(r"^([<>=~!]*)\s*(.+)$", s)
+def _parse_version(v):
+    parts = []
+    for part in re.split(r"[.-]", v):
+        if part[:1] in "0123456789":
+            parts.append(part.zfill(8))
+        else:
+            parts.append("*" + part)
+    parts.append("*z")  # end mark
+    return tuple(parts)
+
+
+class Version:
+    def __init__(self, v):
+        self.v = v
+        self._comp_key = _parse_version(v)
+
+    def __repr__(self):
+        return self.v
+
+    @property
+    def is_prerelease(self):
+        return any(part[0] == "*" for part in self._comp_key[:-1])
+
+    def __len__(self):
+        return len(self._comp_key)
+
+    def __eq__(self, o):
+        if not isinstance(o, Version):
+            return NotImplemented
+        left = self
+        if len(left) < len(o):
+            left = left.pad(len(o) - len(left))
+        elif len(left) > len(o):
+            o = o.pad(len(left) - len(o))
+        return left._comp_key == o._comp_key
+
+    def __lt__(self, o):
+        return self._comp_key < o._comp_key
+
+    def __le__(self, o):
+        return self._comp_key <= o._comp_key
+
+    def __gt__(self, o):
+        return self._comp_key > o._comp_key
+
+    def __ge__(self, o):
+        return self._comp_key >= o._comp_key
+
+    def __hash__(self):
+        return hash(self._comp_key)
+
+    def pad(self, n):
+        return Version(self.v + ".0" * n)
+
+
+def _compatible_gt(a, b):
+    """a ~> b"""
+    if a < b:
+        return False
+    a_digits = [part for part in a._comp_key if part[0] != "*"]
+    b_digits = [part for part in b._comp_key if part[0] != "*"]
+    target_len = len(b_digits)
+    return a_digits[: target_len - 1] == b_digits[: target_len - 1]
+
+
+_compare_ops = {
+    "=": operator.eq,
+    ">": operator.gt,
+    ">=": operator.ge,
+    "<": operator.lt,
+    "<=": operator.le,
+    "~>": _compatible_gt,
+    "!=": operator.ne,
+}
+
+
+def _version_in_spec(version, spec):
+    if not spec:
+        return not version.is_prerelease
+    m = re.match(r"([><=~!]*)\s*(.*)", spec)
     op, ver = m.groups()
-    if not op or op == "=":
-        return "== {}".format(ver)
-    elif op == "~>":
-        if len(ver) == 1:
-            # PEP 440 can't handle "~= X" (no minor part). This translates to
-            # a simple ">= X" because it means we accept major version changes.
-            return ">= {}".format(ver)
-        return "~= {0}, >= {0}".format(ver)
-    return s
+    if not op:
+        op = "="
+    spec_ver = Version(ver)
+    allow_prereleases = spec_ver.is_prerelease
+    if not allow_prereleases and version.is_prerelease:
+        return False
+    if len(spec_ver) > len(version):
+        version = version.pad(len(spec_ver) - len(version))
+    return _compare_ops[op](version, spec_ver)
 
 
 def _iter_convert_specifiers(inp):
     for raw in inp.split(","):
-        cov = _convert_specifier(raw.strip())
-        if not cov:
-            continue
-        yield cov
+        yield raw.strip()
 
 
-def _parse_specifier_set(inp):
-    return packaging.specifiers.SpecifierSet(
-        ", ".join(_iter_convert_specifiers(inp)),
-    )
+def _version_in_specset(version, specset):
+    for spec in _iter_convert_specifiers(specset):
+        if not _version_in_spec(version, spec):
+            return False
+    return True
 
 
 def _safe_json_load(filename):
@@ -74,7 +144,7 @@
 
 def _iter_resolved(dependencies):
     for entry in dependencies:
-        yield (entry["name"], packaging.version.parse(entry["version"]))
+        yield (entry["name"], Version(entry["version"]))
         for sub in _iter_resolved(entry["dependencies"]):
             yield sub
 
@@ -91,11 +161,11 @@
         self.index = _safe_json_load(index_name)
 
         self.root_requirements = [
-            Requirement(_clean_identifier(key), _parse_specifier_set(spec))
+            Requirement(_clean_identifier(key), spec)
             for key, spec in case_data["requested"].items()
         ]
         self.pinned_versions = {
-            entry["name"]: packaging.version.parse(entry["version"])
+            entry["name"]: Version(entry["version"])
             for entry in case_data["base"]
         }
         self.expected_resolution = dict(_iter_resolved(case_data["resolved"]))
@@ -121,21 +191,20 @@
             return
         bad_versions = {c.ver for c in incompatibilities[name]}
         for entry in data:
-            version = packaging.version.parse(entry["version"])
-            if any(version not in r.spec for r in requirements[name]):
+            version = Version(entry["version"])
+            if any(
+                not _version_in_specset(version, r.spec)
+                for r in requirements[name]
+            ):
                 continue
             if version in bad_versions:
                 continue
             # Some fixtures incorrectly set dependencies to an empty list.
             dependencies = entry["dependencies"] or {}
-            dependencies = [
-                Requirement(k, _parse_specifier_set(v))
-                for k, v in dependencies.items()
-            ]
+            dependencies = [Requirement(k, v) for k, v in dependencies.items()]
             yield Candidate(entry["name"], version, dependencies)
 
     def find_matches(self, identifier, requirements, incompatibilities):
-
         candidates = sorted(
             self._iter_matches(identifier, requirements, incompatibilities),
             key=operator.attrgetter("ver"),
@@ -148,7 +217,7 @@
             yield c
 
     def is_satisfied_by(self, requirement, candidate):
-        return candidate.ver in requirement.spec
+        return _version_in_specset(candidate.ver, requirement.spec)
 
     def get_dependencies(self, candidate):
         return candidate.deps
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-1.0.1/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-1.0.1/tests/functional/python/inputs/index/same-package.json 
2023-03-09 06:10:19.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-1.0.1/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-1.0.1/tests/functional/python/test_resolvers_python.py       
2023-03-09 06:10:19.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-1.0.1/tests/test_resolvers.py
--- old/resolvelib-0.8.1/tests/test_resolvers.py        2021-10-11 
23:05:57.000000000 +0200
+++ new/resolvelib-1.0.1/tests/test_resolvers.py        2023-03-09 
06:10:19.000000000 +0100
@@ -1,4 +1,21 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    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,
@@ -8,6 +25,17 @@
     Resolver,
 )
 
+if TYPE_CHECKING:
+    from resolvelib.resolvers import (
+        Criterion,
+        RequirementInformation,
+        RequirementsConflicted,
+    )
+
+from collections import namedtuple
+
+from resolvelib.resolvers import Resolution
+
 
 def test_candidate_inconsistent_error():
     requirement = "foo"
@@ -95,10 +123,21 @@
 
 
 def test_resolving_conflicts():
+    Candidate = namedtuple(
+        "Candidate", ["name", "version", "requirements"]
+    )  # name, version, requirements
+    _Requirement = namedtuple(
+        "Requirement", ["name", "versions"]
+    )  # name, versions
+    a1 = Candidate("a", 1, [_Requirement("q", {1})])
+    a2 = Candidate("a", 2, [_Requirement("q", {2})])
+    b = Candidate("b", 1, [_Requirement("q", {1})])
+    q1 = Candidate("q", 1, [])
+    q2 = Candidate("q", 2, [])
     all_candidates = {
-        "a": [("a", 1, [("q", {1})]), ("a", 2, [("q", {2})])],
-        "b": [("b", 1, [("q", {1})])],
-        "q": [("q", 1, []), ("q", 2, [])],
+        "a": [a1, a2],
+        "b": [b],
+        "q": [q1, q2],
     }
 
     class Reporter(BaseReporter):
@@ -116,20 +155,22 @@
             return 0
 
         def get_dependencies(self, candidate):
-            return candidate[2]
+            return candidate.requirements
 
         def find_matches(self, identifier, requirements, incompatibilities):
-            bad_versions = {c[1] for c in incompatibilities[identifier]}
+            bad_versions = {c.version for c in incompatibilities[identifier]}
             candidates = [
                 c
                 for c in all_candidates[identifier]
-                if all(c[1] in r[1] for r in requirements[identifier])
-                and c[1] not in bad_versions
+                if all(
+                    c.version in r.versions for r in requirements[identifier]
+                )
+                and c.version not in bad_versions
             ]
-            return sorted(candidates, key=lambda c: c[1], reverse=True)
+            return sorted(candidates, key=lambda c: c.version, reverse=True)
 
         def is_satisfied_by(self, requirement, candidate):
-            return candidate[1] in requirement[1]
+            return candidate.version in requirement.versions
 
     def run_resolver(*args):
         reporter = Reporter()
@@ -140,6 +181,117 @@
         except ResolutionImpossible as e:
             return e.causes
 
-    backtracking_causes = run_resolver([("a", {1, 2}), ("b", {1})])
-    exception_causes = run_resolver([("a", {2}), ("b", {1})])
+    backtracking_causes = run_resolver(
+        [_Requirement("a", {1, 2}), _Requirement("b", {1})]
+    )
+    exception_causes = run_resolver(
+        [_Requirement("a", {2}), _Requirement("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).
+    """
+    if TYPE_CHECKING:
+        Candidate = Tuple[  # noqa: F841
+            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