diff -Nru python-resolvelib-0.8.1/CHANGELOG.rst python-resolvelib-0.5.4/CHANGELOG.rst --- python-resolvelib-0.8.1/CHANGELOG.rst 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/CHANGELOG.rst 2020-12-27 12:17:14.000000000 +0000 @@ -1,87 +1,3 @@ -0.8.1 (2021-10-12) -================== - -Features --------- - -- A new reporter hook ``resolving_conflicts`` is added. The resolver triggers - this hook when it detects conflicts in the dependency tree, and before it - attempts to fix them. The hook accepts one single argument ``causes``, which - is a list of ``(requirement, parent)`` 2-tuples that represents all the - edges that lead to the detected conflicts. `#81 `_ - - -0.8.0 (2021-10-08) -================== - -Features --------- - -- Add ``backtrack_causes`` to ``get_preference``, which contains information - about the requirements involved in the most recent backtrack. This allows - the provider to utilise this information to tweak the ordering as well as - for recording/reporting conflicts. - - -0.7.1 (2021-06-22) -================== - -Bug Fixes ---------- - -- When merging a candidate's dependencies, make sure the merge target is - up-to-date within the loop, so the merge does not lose information when a - candidate returns multiple dependency specifications under one identifier - (e.g. specifyiung two dependencies ``a>1`` and ``a<2``, instead of one single - ``a>1,<2`` dependency). `#80 `_ - - -0.7.0 (2021-04-13) -================== - -Features --------- - -- Redesign ``get_preference()`` to include resolution state on dependencies - other than the currently working one, to allow the provider to better take - account of the global resolver knowledge and determine the best strategy. The - provider now can, for example, correctly calculate how far a dependency is - from the root node in the graph. `#74 `_ - - -0.6.0 (2021-04-04) -================== - -Features --------- - -- A new argument ``incompatibilities`` is now passed to the ``find_matches()`` - hook, which the provider must use to exclude matches from the return value. `#68 `_ - -- Redesign ``find_matches()`` to include resolution state on dependencies other - than the currently working one, to handle usages that need to return candidates - based on non-local states. One such example is PEP 508 direct URLs specified - on a package, which need to be available to the same package specified with - extras (which would have a different identifier). `#74 `_ - - -Bug Fixes ---------- - -- The resolver no longer relies on implicit candidate equality to detect - incompatibilities. This is done by an additional ``find_matches()`` argument; - see the *Features* section to learn more. `#68 `_ - - -0.5.5 (2021-03-09) -================== - -Features --------- - -- Provide type stubs for most classes. `#72 `_ - - 0.5.4 (2020-12-27) ================== diff -Nru python-resolvelib-0.8.1/debian/changelog python-resolvelib-0.5.4/debian/changelog --- python-resolvelib-0.8.1/debian/changelog 2022-01-14 13:24:40.000000000 +0000 +++ python-resolvelib-0.5.4/debian/changelog 2022-01-17 09:16:38.000000000 +0000 @@ -1,19 +1,8 @@ -python-resolvelib (0.8.1-1~ubuntu20.04) focal; urgency=medium +python-resolvelib (0.5.4-1~ubuntu20.04) focal; urgency=medium * Backport to focal - -- Nafallo Bjälevik Fri, 14 Jan 2022 14:24:40 +0100 - -python-resolvelib (0.8.1-1) unstable; urgency=medium - - * New upstream release - * Add python3-commentjson and python3-pytest to build-depends so tests are - run during build - * Add python3-commentjson to test depends and update autopkgtest so - cocoapods tests are run during autopkgtest - * Bump standards-version to 4.6.0 without further change - - -- Scott Kitterman Mon, 29 Nov 2021 01:58:38 -0500 + -- Nafallo Bjälevik Mon, 17 Jan 2022 10:16:38 +0100 python-resolvelib (0.5.4-1) unstable; urgency=medium diff -Nru python-resolvelib-0.8.1/debian/control python-resolvelib-0.5.4/debian/control --- python-resolvelib-0.8.1/debian/control 2022-01-14 13:23:49.000000000 +0000 +++ python-resolvelib-0.5.4/debian/control 2022-01-17 09:15:04.000000000 +0000 @@ -4,13 +4,11 @@ Maintainer: Nafallo Bjälevik XSBC-Original-Maintainer: Debian Python Team Uploaders: Scott Kitterman -Standards-Version: 4.6.0 +Standards-Version: 4.5.0 Build-Depends: debhelper-compat (= 12), dh-python, python3-all, - python3-commentjson, - python3-pytest, python3-setuptools, Homepage: https://github.com/sarugaku/resolvelib Vcs-Git: https://salsa.debian.org/python-team/packages/python-resolvelib.git diff -Nru python-resolvelib-0.8.1/debian/tests/control python-resolvelib-0.5.4/debian/tests/control --- python-resolvelib-0.8.1/debian/tests/control 2021-11-29 06:58:38.000000000 +0000 +++ python-resolvelib-0.5.4/debian/tests/control 2021-03-01 21:04:34.000000000 +0000 @@ -1,3 +1,3 @@ Tests: run-pytest -Depends: @, python3-all, python3-pytest, python3-commentjson +Depends: @, python3-all, python3-pytest Restrictions: allow-stderr diff -Nru python-resolvelib-0.8.1/debian/tests/run-pytest python-resolvelib-0.5.4/debian/tests/run-pytest --- python-resolvelib-0.8.1/debian/tests/run-pytest 2021-11-29 06:58:38.000000000 +0000 +++ python-resolvelib-0.5.4/debian/tests/run-pytest 2021-03-01 21:04:34.000000000 +0000 @@ -8,6 +8,8 @@ mkdir -p xxx cp -a tests xxx/ cd xxx +# Until python3-commentjson is in the archive: +rm -rf tests/functional/cocoapods # Test for all supported python3 versions so we find out if there are any # issues with a new python3 version before it is the default version. diff -Nru python-resolvelib-0.8.1/DEVELOPMENT.rst python-resolvelib-0.5.4/DEVELOPMENT.rst --- python-resolvelib-0.8.1/DEVELOPMENT.rst 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/DEVELOPMENT.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -=========== -Development -=========== - -ResolveLib is a volunteer maintained open source project and we welcome contributions of all forms. -The sections below will help you get started with development, testing, and documentation. - - -Getting Started -=============== - -The first thing to do is to fork this repository, install with test dependencies and run tests. - - -.. code-block:: shell - - python -m pip install .[test] - - python -m pytest - - -Submitting Pull Requests -======================== - -Please make sure any changes are covered by existing tests or that new tests are added. -ResolveLib is used on many different python versions and operating systems and environments so every effort must be made in order to keep code portable. -Pull requests should be small to facilitate easier review. - - -Release Process for Maintainers -=============================== - -Replace ``X.Y.Z`` with the release you would like to make. - -* Make sure the news fragments are in place. -* ``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`` - -Breakdown of the ``release`` nox task: - -* Writes ``X.Y.Z`` to ``src/resolvelib/__init__.py``. -* Runs ``towncrier`` to update the changelog and delete news fragments. -* Commit the changelog and version change. -* Tag the commit as release ``X.Y.Z``. -* Build, check, and upload distributions to the index specified by ``repo``. -* Writes ``X.Y.Z+1.dev0`` to ``src/resolvelib/__init__.py``. -* Commit the "prebump" change. diff -Nru python-resolvelib-0.8.1/.editorconfig python-resolvelib-0.5.4/.editorconfig --- python-resolvelib-0.8.1/.editorconfig 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/.editorconfig 2020-12-27 12:17:14.000000000 +0000 @@ -10,7 +10,7 @@ [*.md] trim_trailing_whitespace = false -[*.{py,rst,yml,pyi}] +[*.{py,rst,yml}] indent_style = space [*.{ini,json,toml,yml}] diff -Nru python-resolvelib-0.8.1/examples/pypi_wheel_provider.py python-resolvelib-0.5.4/examples/pypi_wheel_provider.py --- python-resolvelib-0.8.1/examples/pypi_wheel_provider.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/examples/pypi_wheel_provider.py 2020-12-27 12:17:14.000000000 +0000 @@ -7,16 +7,15 @@ from urllib.parse import urlparse from zipfile import ZipFile -import html5lib import requests -from packaging.requirements import Requirement +import html5lib from packaging.specifiers import SpecifierSet +from packaging.version import Version, InvalidVersion +from packaging.requirements import Requirement from packaging.utils import canonicalize_name -from packaging.version import InvalidVersion, Version - from resolvelib import BaseReporter, Resolver -from .extras_provider import ExtrasProvider +from extras_provider import ExtrasProvider PYTHON_VERSION = Version(python_version()) @@ -122,26 +121,25 @@ def get_base_requirement(self, candidate): return Requirement("{}=={}".format(candidate.name, candidate.version)) - def get_preference(self, identifier, resolutions, candidates, information): - return sum(1 for _ in candidates[identifier]) + def get_preference(self, resolution, candidates, information): + return len(candidates) - def find_matches(self, identifier, requirements, incompatibilities): - requirements = list(requirements[identifier]) + def find_matches(self, requirements): + assert requirements, "resolver promises at least one requirement" assert not any( - r.extras for r in requirements + r.extras for r in requirements[1:] ), "extras not supported in this example" - bad_versions = {c.version for c in incompatibilities[identifier]} + name = canonicalize_name(requirements[0].name) # Need to pass the extras to the search, so they # are added to the candidate at creation - we # treat candidates as immutable once created. - candidates = ( - candidate - for candidate in get_project_from_pypi(identifier, set()) - if candidate.version not in bad_versions - and all(candidate.version in r.specifier for r in requirements) - ) + candidates = [] + for c in get_project_from_pypi(name, set()): + version = c.version + if all(version in r.specifier for r in requirements): + candidates.append(c) return sorted(candidates, key=attrgetter("version"), reverse=True) def is_satisfied_by(self, requirement, candidate): diff -Nru python-resolvelib-0.8.1/examples/reporter_demo.py python-resolvelib-0.5.4/examples/reporter_demo.py --- python-resolvelib-0.8.1/examples/reporter_demo.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/examples/reporter_demo.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,5 +1,4 @@ from collections import namedtuple - from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -73,17 +72,17 @@ def identify(self, requirement_or_candidate): return requirement_or_candidate.name - def get_preference(self, identifier, resolutions, candidates, information): - return sum(1 for _ in candidates[identifier]) + def get_preference(self, resolution, candidates, information): + return len(candidates) - def find_matches(self, identifier, requirements, incompatibilities): - name = identifier - return sorted( - c - for c in self.candidates - if all(self.is_satisfied_by(r, c) for r in requirements[name]) - and all(c.version != i.version for i in incompatibilities[name]) + def find_matches(self, requirement): + deps = list( + filter( + lambda candidate: self.is_satisfied_by(requirement, candidate), + sorted(self.candidates), + ) ) + return deps def is_satisfied_by(self, requirement, candidate): return ( diff -Nru python-resolvelib-0.8.1/examples/visualization/run.py python-resolvelib-0.5.4/examples/visualization/run.py --- python-resolvelib-0.8.1/examples/visualization/run.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/examples/visualization/run.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,9 +1,9 @@ import re import sys -from ..reporter_demo import Candidate, Requirement -from .generate import generate_html -from .reporter import GraphGeneratingReporter +from reporter_demo import Candidate, Requirement +from visualization.generate import generate_html +from visualization.reporter import GraphGeneratingReporter def process_arguments(function, args): diff -Nru python-resolvelib-0.8.1/examples/visualization/run_pypi.py python-resolvelib-0.5.4/examples/visualization/run_pypi.py --- python-resolvelib-0.8.1/examples/visualization/run_pypi.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/examples/visualization/run_pypi.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,9 +1,8 @@ from pypi_wheel_provider import PyPIProvider, Requirement +from resolvelib import Resolver from visualization.generate import generate_html from visualization.reporter import GraphGeneratingReporter -from resolvelib import Resolver - if __name__ == "__main__": provider = PyPIProvider() reporter = GraphGeneratingReporter() diff -Nru python-resolvelib-0.8.1/.github/workflows/ci.yml python-resolvelib-0.5.4/.github/workflows/ci.yml --- python-resolvelib-0.8.1/.github/workflows/ci.yml 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/.github/workflows/ci.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -name: CI -on: - push: - branches: [main] - pull_request: - -jobs: - lint: - 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/ - 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 . - test: - name: Test - runs-on: ubuntu-latest - needs: [lint] - strategy: - fail-fast: true - matrix: - python: - - "2.7" - - "3.10" - - "3.9" - - "3.8" - - "3.7" - - "3.6" - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - run: pip install .[test] - - run: pytest tests diff -Nru python-resolvelib-0.8.1/.gitignore python-resolvelib-0.5.4/.gitignore --- python-resolvelib-0.8.1/.gitignore 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/.gitignore 2020-12-27 12:17:14.000000000 +0000 @@ -6,7 +6,6 @@ .nox .vscode .mypy_cache -venv/ build dist diff -Nru python-resolvelib-0.8.1/MANIFEST.in python-resolvelib-0.5.4/MANIFEST.in --- python-resolvelib-0.8.1/MANIFEST.in 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/MANIFEST.in 2020-12-27 12:17:14.000000000 +0000 @@ -1,2 +1 @@ include CHANGELOG.rst LICENSE -recursive-include src *.pyi py.typed diff -Nru python-resolvelib-0.8.1/noxfile.py python-resolvelib-0.5.4/noxfile.py --- python-resolvelib-0.8.1/noxfile.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/noxfile.py 2020-12-27 12:17:14.000000000 +0000 @@ -4,6 +4,7 @@ import nox + ROOT = pathlib.Path(__file__).resolve().parent INIT_PY = ROOT.joinpath("src", "resolvelib", "__init__.py") @@ -14,12 +15,10 @@ @nox.session def lint(session): - session.install(".[lint, test]") + session.install(".[lint]") session.run("black", "--check", ".") - session.run("isort", ".") session.run("flake8", ".") - session.run("mypy", "src", "tests") @nox.session(python=["3.9", "3.8", "3.7", "3.6", "3.5", "2.7"]) @@ -46,15 +45,6 @@ f.write("".join(lines)) -SAFE_RMTREE = """ -import os -import shutil - -if os.path.isdir({path!r}): - shutil.rmtree({path!r}) -""" - - @nox.session def release(session): session.install(".[release]") @@ -103,25 +93,12 @@ else: session.log("Skipping preprocessing since --version is empty") - session.log("Cleaning dist/ content...") - session.run("python", "-c", SAFE_RMTREE.format(path="dist")) - - session.log("Building distributions...") - session.run("python", "-m", "build") - session.run("twine", "check", "dist/*") - if options.repo: session.log(f"Releasing distributions to {options.repo}...") + session.run("setl", "publish", "--repository", options.repo) else: - session.log("Storing distributions locally since --repo is empty") - if options.repo: - session.run( - "twine", - "upload", - "--repository-url", - options.repo, - "dist/*", - ) + session.log("Building distributions locally since --repo is empty") + session.run("setl", "publish", "--no-upload") if options.prebump: _write_package_version(options.prebump) diff -Nru python-resolvelib-0.8.1/pyproject.toml python-resolvelib-0.5.4/pyproject.toml --- python-resolvelib-0.8.1/pyproject.toml 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/pyproject.toml 2020-12-27 12:17:14.000000000 +0000 @@ -5,11 +5,6 @@ line-length = 79 include = '^/(docs|examples|src|tasks|tests)/.+\.py$' -[tool.isort] -profile = "black" -line_length = 79 -multi_line_output = 3 - [tool.towncrier] package = 'resolvelib' package_dir = 'src' diff -Nru python-resolvelib-0.8.1/README.rst python-resolvelib-0.5.4/README.rst --- python-resolvelib-0.8.1/README.rst 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/README.rst 2020-12-27 12:17:14.000000000 +0000 @@ -83,8 +83,3 @@ "dependency", if not clarified otherwise, also refers to this concept. A Requirement should specify two things: a Package, and a Specifier. - -Contributing -============ - -Please see `developer documentation <./DEVELOPMENT.rst>`__. diff -Nru python-resolvelib-0.8.1/setup.cfg python-resolvelib-0.5.4/setup.cfg --- python-resolvelib-0.8.1/setup.cfg 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/setup.cfg 2020-12-27 12:17:14.000000000 +0000 @@ -38,17 +38,13 @@ lint = black flake8 - mypy - isort - types-requests test = commentjson packaging pytest release = - build + setl towncrier - twine [bdist_wheel] universal = 1 @@ -65,4 +61,3 @@ __pycache__, build, dist, - *.pyi diff -Nru python-resolvelib-0.8.1/setup.py python-resolvelib-0.5.4/setup.py --- python-resolvelib-0.8.1/setup.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/setup.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,3 +1,2 @@ from setuptools import setup - setup() diff -Nru python-resolvelib-0.8.1/src/resolvelib/compat/collections_abc.py python-resolvelib-0.5.4/src/resolvelib/compat/collections_abc.py --- python-resolvelib-0.8.1/src/resolvelib/compat/collections_abc.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/compat/collections_abc.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,6 +1,6 @@ -__all__ = ["Mapping", "Sequence"] +__all__ = ["Sequence"] try: - from collections.abc import Mapping, Sequence + from collections.abc import Sequence except ImportError: - from collections import Mapping, Sequence + from collections import Sequence diff -Nru python-resolvelib-0.8.1/src/resolvelib/__init__.py python-resolvelib-0.5.4/src/resolvelib/__init__.py --- python-resolvelib-0.8.1/src/resolvelib/__init__.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/__init__.py 2020-12-27 12:17:14.000000000 +0000 @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.8.1" +__version__ = "0.5.4" from .providers import AbstractProvider, AbstractResolver @@ -19,8 +19,8 @@ from .resolvers import ( InconsistentCandidate, RequirementsConflicted, + Resolver, ResolutionError, ResolutionImpossible, ResolutionTooDeep, - Resolver, ) diff -Nru python-resolvelib-0.8.1/src/resolvelib/__init__.pyi python-resolvelib-0.5.4/src/resolvelib/__init__.pyi --- python-resolvelib-0.8.1/src/resolvelib/__init__.pyi 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/__init__.pyi 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -__version__: str - -from .providers import AbstractProvider as AbstractProvider -from .providers import AbstractResolver as AbstractResolver -from .reporters import BaseReporter as BaseReporter -from .resolvers import InconsistentCandidate as InconsistentCandidate -from .resolvers import RequirementsConflicted as RequirementsConflicted -from .resolvers import ResolutionError as ResolutionError -from .resolvers import ResolutionImpossible as ResolutionImpossible -from .resolvers import ResolutionTooDeep as ResolutionTooDeep -from .resolvers import Resolver as Resolver diff -Nru python-resolvelib-0.8.1/src/resolvelib/providers.py python-resolvelib-0.5.4/src/resolvelib/providers.py --- python-resolvelib-0.8.1/src/resolvelib/providers.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/providers.py 2020-12-27 12:17:14.000000000 +0000 @@ -2,45 +2,37 @@ """Delegate class to provide requirement interface for the resolver.""" def identify(self, requirement_or_candidate): - """Given a requirement, return an identifier for it. + """Given a requirement or candidate, return an identifier for it. - This is used to identify a requirement, e.g. whether two requirements - should have their specifier parts merged. + This is used in many places to identify a requirement or candidate, + e.g. whether two requirements should have their specifier parts merged, + whether two candidates would conflict with each other (because they + have same name but different versions). """ raise NotImplementedError - def get_preference( - self, - identifier, - resolutions, - candidates, - information, - backtrack_causes, - ): + def get_preference(self, resolution, candidates, information): """Produce a sort key for given requirement based on preference. The preference is defined as "I think this requirement should be resolved first". The lower the return value is, the more preferred this group of arguments is. - :param identifier: An identifier as returned by ``identify()``. This - identifies the dependency matches of which should be returned. - :param resolutions: Mapping of candidates currently pinned by the - resolver. Each key is an identifier, and the value 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. - :param information: Mapping of requirement information of each package. - Each value is an iterator of *requirement information*. - :param backtrack_causes: Sequence of requirement information that were - the requirements that caused the resolver to most recently backtrack. - - A *requirement information* instance is a named tuple with two members: - - * ``requirement`` specifies a requirement contributing to the current - list of candidates. - * ``parent`` specifies the candidate that provides (dependend on) the - requirement, or ``None`` to indicate a root requirement. + :param resolution: Currently pinned candidate, or `None`. + :param candidates: An iterable of possible candidates. + :param information: A list of requirement information. + + The `candidates` iterable's exact type depends on the return type of + `find_matches()`. A sequence is passed-in as-is if possible. If it + returns a callble, the iterator returned by that callable is passed + in here. + + Each element in `information` is a named tuple with two entries: + + * `requirement` specifies a requirement contributing to the current + candidate list. + * `parent` specifies the candidate that provides (dependend on) the + requirement, or `None` to indicate a root requirement. The preference could depend on a various of issues, including (not necessarily in this order): @@ -53,25 +45,15 @@ * Are there any known conflicts for this requirement? We should probably work on those with the most known conflicts. - A sortable value should be returned (this will be used as the ``key`` + A sortable value should be returned (this will be used as the `key` parameter of the built-in sorting function). The smaller the value is, the more preferred this requirement is (i.e. the sorting function - is called with ``reverse=False``). + is called with `reverse=False`). """ raise NotImplementedError - def find_matches(self, identifier, requirements, incompatibilities): - """Find all possible candidates that satisfy given constraints. - - :param identifier: An identifier as returned by ``identify()``. This - identifies the dependency matches of which should be returned. - :param requirements: A mapping of requirements that all returned - candidates must satisfy. Each key is an identifier, and the value - an iterator of requirements for that dependency. - :param incompatibilities: A mapping of known incompatibilities of - each dependency. Each key is an identifier, and the value an - iterator of incompatibilities known to the resolver. All - incompatibilities *must* be excluded from the return value. + def find_matches(self, requirements): + """Find all possible candidates that satisfy the given requirements. This should try to get candidates based on the requirements' types. For VCS, local, and archive requirements, the one-and-only match is @@ -86,6 +68,10 @@ * An collection of candidates. * An iterable of candidates. This will be consumed immediately into a list of candidates. + + :param requirements: A collection of requirements which all of the + returned candidates must match. All requirements are guaranteed to + have the same identifier. The collection is never empty. """ raise NotImplementedError @@ -95,7 +81,7 @@ The candidate is guarenteed to have been generated from the requirement. - A boolean should be returned to indicate whether ``candidate`` is a + A boolean should be returned to indicate whether `candidate` is a viable solution to the requirement. """ raise NotImplementedError diff -Nru python-resolvelib-0.8.1/src/resolvelib/providers.pyi python-resolvelib-0.5.4/src/resolvelib/providers.pyi --- python-resolvelib-0.8.1/src/resolvelib/providers.pyi 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/providers.pyi 1970-01-01 00:00:00.000000000 +0000 @@ -1,44 +0,0 @@ -from typing import ( - Any, - Collection, - Generic, - Iterable, - Iterator, - Mapping, - Optional, - Protocol, - Union, -) - -from .reporters import BaseReporter -from .resolvers import RequirementInformation -from .structs import CT, KT, RT, Matches - -class Preference(Protocol): - def __lt__(self, __other: Any) -> bool: ... - -class AbstractProvider(Generic[RT, CT, KT]): - def identify(self, requirement_or_candidate: Union[RT, CT]) -> KT: ... - def get_preference( - self, - identifier: KT, - resolutions: Mapping[KT, CT], - candidates: Mapping[KT, Iterator[CT]], - information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]], - ) -> Preference: ... - def find_matches( - self, - identifier: KT, - requirements: Mapping[KT, Iterator[RT]], - incompatibilities: Mapping[KT, Iterator[CT]], - ) -> Matches: ... - def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: ... - def get_dependencies(self, candidate: CT) -> Iterable[RT]: ... - -class AbstractResolver(Generic[RT, CT, KT]): - base_exception = Exception - provider: AbstractProvider[RT, CT, KT] - reporter: BaseReporter - def __init__( - self, provider: AbstractProvider[RT, CT, KT], reporter: BaseReporter - ): ... diff -Nru python-resolvelib-0.8.1/src/resolvelib/reporters.py python-resolvelib-0.5.4/src/resolvelib/reporters.py --- python-resolvelib-0.8.1/src/resolvelib/reporters.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/reporters.py 2020-12-27 12:17:14.000000000 +0000 @@ -30,12 +30,6 @@ requirements passed in from ``Resolver.resolve()``. """ - def resolving_conflicts(self, causes): - """Called when starting to attempt requirement conflict resolution. - - :param causes: The information on the collision that caused the backtracking. - """ - def backtracking(self, candidate): """Called when rejecting a candidate during backtracking.""" diff -Nru python-resolvelib-0.8.1/src/resolvelib/reporters.pyi python-resolvelib-0.5.4/src/resolvelib/reporters.pyi --- python-resolvelib-0.8.1/src/resolvelib/reporters.pyi 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/reporters.pyi 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -from typing import Any - -class BaseReporter: - def starting(self) -> Any: ... - def starting_round(self, index: int) -> Any: ... - 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 resolving_conflicts(self, causes: Any) -> Any: ... - def pinning(self, candidate: Any) -> Any: ... diff -Nru python-resolvelib-0.8.1/src/resolvelib/resolvers.py python-resolvelib-0.5.4/src/resolvelib/resolvers.py --- python-resolvelib-0.8.1/src/resolvelib/resolvers.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/resolvers.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,8 +1,8 @@ import collections -import operator from .providers import AbstractResolver -from .structs import DirectedGraph, IteratorMapping, build_iter_view +from .structs import DirectedGraph, build_iter_view + RequirementInformation = collections.namedtuple( "RequirementInformation", ["requirement", "parent"] @@ -73,12 +73,43 @@ ) return "Criterion({})".format(requirements) + @classmethod + def from_requirement(cls, provider, requirement, parent): + """Build an instance from a requirement.""" + cands = build_iter_view(provider.find_matches([requirement])) + infos = [RequirementInformation(requirement, parent)] + criterion = cls(cands, infos, incompatibilities=[]) + if not cands: + raise RequirementsConflicted(criterion) + return criterion + def iter_requirement(self): return (i.requirement for i in self.information) def iter_parent(self): return (i.parent for i in self.information) + def merged_with(self, provider, requirement, parent): + """Build a new instance from this and a new requirement.""" + infos = list(self.information) + infos.append(RequirementInformation(requirement, parent)) + cands = build_iter_view(provider.find_matches([r for r, _ in infos])) + criterion = type(self)(cands, infos, list(self.incompatibilities)) + if not cands: + raise RequirementsConflicted(criterion) + return criterion + + def excluded_of(self, candidates): + """Build a new instance from this, but excluding specified candidates. + + Returns the new instance, or None if we still have no valid candidates. + """ + cands = self.candidates.excluding(candidates) + if not cands: + return None + incompats = self.incompatibilities + candidates + return type(self)(cands, list(self.information), incompats) + class ResolutionError(ResolverException): pass @@ -98,7 +129,7 @@ # Resolution state in a round. -State = collections.namedtuple("State", "mapping criteria backtrack_causes") +State = collections.namedtuple("State", "mapping criteria") class Resolution(object): @@ -130,62 +161,26 @@ state = State( mapping=base.mapping.copy(), criteria=base.criteria.copy(), - backtrack_causes=base.backtrack_causes[:], ) self._states.append(state) - def _add_to_criteria(self, criteria, requirement, parent): - self._r.adding_requirement(requirement=requirement, parent=parent) - - identifier = self._p.identify(requirement_or_candidate=requirement) - criterion = criteria.get(identifier) - if criterion: - incompatibilities = list(criterion.incompatibilities) - else: - incompatibilities = [] - - matches = self._p.find_matches( - identifier=identifier, - requirements=IteratorMapping( - criteria, - operator.methodcaller("iter_requirement"), - {identifier: [requirement]}, - ), - incompatibilities=IteratorMapping( - criteria, - operator.attrgetter("incompatibilities"), - {identifier: incompatibilities}, - ), - ) - - if criterion: - information = list(criterion.information) - information.append(RequirementInformation(requirement, parent)) + def _merge_into_criterion(self, requirement, parent): + self._r.adding_requirement(requirement, parent) + name = self._p.identify(requirement) + try: + crit = self.state.criteria[name] + except KeyError: + crit = Criterion.from_requirement(self._p, requirement, parent) else: - information = [RequirementInformation(requirement, parent)] - - criterion = Criterion( - candidates=build_iter_view(matches), - information=information, - incompatibilities=incompatibilities, - ) - if not criterion.candidates: - raise RequirementsConflicted(criterion) - criteria[identifier] = criterion + crit = crit.merged_with(self._p, requirement, parent) + return name, crit - def _get_preference(self, name): + def _get_criterion_item_preference(self, item): + name, criterion = item return self._p.get_preference( - identifier=name, - resolutions=self.state.mapping, - candidates=IteratorMapping( - self.state.criteria, - operator.attrgetter("candidates"), - ), - information=IteratorMapping( - self.state.criteria, - operator.attrgetter("information"), - ), - backtrack_causes=self.state.backtrack_causes, + self.state.mapping.get(name), + criterion.candidates.for_preference(), + criterion.information, ) def _is_current_pin_satisfying(self, name, criterion): @@ -194,23 +189,22 @@ except KeyError: return False return all( - self._p.is_satisfied_by(requirement=r, candidate=current_pin) + self._p.is_satisfied_by(r, current_pin) for r in criterion.iter_requirement() ) - def _get_updated_criteria(self, candidate): - criteria = self.state.criteria.copy() - for requirement in self._p.get_dependencies(candidate=candidate): - self._add_to_criteria(criteria, requirement, parent=candidate) + def _get_criteria_to_update(self, candidate): + criteria = {} + for r in self._p.get_dependencies(candidate): + name, crit = self._merge_into_criterion(r, parent=candidate) + criteria[name] = crit return criteria - def _attempt_to_pin_criterion(self, name): - criterion = self.state.criteria[name] - + def _attempt_to_pin_criterion(self, name, criterion): causes = [] for candidate in criterion.candidates: try: - criteria = self._get_updated_criteria(candidate) + criteria = self._get_criteria_to_update(candidate) except RequirementsConflicted as e: causes.append(e.criterion) continue @@ -220,19 +214,18 @@ # faulty provider, we will raise an error to notify the implementer # to fix find_matches() and/or is_satisfied_by(). satisfied = all( - self._p.is_satisfied_by(requirement=r, candidate=candidate) + self._p.is_satisfied_by(r, candidate) for r in criterion.iter_requirement() ) if not satisfied: raise InconsistentCandidate(candidate, criterion) - self._r.pinning(candidate=candidate) - self.state.criteria.update(criteria) - # Put newly-pinned candidate at the end. This is essential because # backtracking looks at this mapping to get the last pin. + self._r.pinning(candidate) self.state.mapping.pop(name, None) self.state.mapping[name] = candidate + self.state.criteria.update(criteria) return [] @@ -274,14 +267,14 @@ broken_state = self._states.pop() name, candidate = broken_state.mapping.popitem() incompatibilities_from_broken = [ - (k, list(v.incompatibilities)) + (k, v.incompatibilities) for k, v in broken_state.criteria.items() ] # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - self._r.backtracking(candidate=candidate) + self._r.backtracking(candidate) # Create a new state from the last known-to-work one, and apply # the previously gathered incompatibility information. @@ -293,27 +286,10 @@ criterion = self.state.criteria[k] except KeyError: continue - matches = self._p.find_matches( - identifier=k, - requirements=IteratorMapping( - self.state.criteria, - operator.methodcaller("iter_requirement"), - ), - incompatibilities=IteratorMapping( - self.state.criteria, - operator.attrgetter("incompatibilities"), - {k: incompatibilities}, - ), - ) - candidates = build_iter_view(matches) - if not candidates: + criterion = criterion.excluded_of(incompatibilities) + if criterion is None: return False - incompatibilities.extend(criterion.incompatibilities) - self.state.criteria[k] = Criterion( - candidates=candidates, - information=list(criterion.information), - incompatibilities=incompatibilities, - ) + self.state.criteria[k] = criterion return True self._push_new_state() @@ -336,18 +312,13 @@ self._r.starting() # Initialize the root state. - self._states = [ - State( - mapping=collections.OrderedDict(), - criteria={}, - backtrack_causes=[], - ) - ] + self._states = [State(mapping=collections.OrderedDict(), criteria={})] for r in requirements: try: - self._add_to_criteria(self.state.criteria, r, parent=None) + name, crit = self._merge_into_criterion(r, parent=None) except RequirementsConflicted as e: raise ResolutionImpossible(e.criterion.information) + self.state.criteria[name] = crit # The root state is saved as a sentinel so the first ever pin can have # something to backtrack to if it fails. The root state is basically @@ -355,39 +326,40 @@ self._push_new_state() for round_index in range(max_rounds): - self._r.starting_round(index=round_index) + self._r.starting_round(round_index) - unsatisfied_names = [ - key - for key, criterion in self.state.criteria.items() - if not self._is_current_pin_satisfying(key, criterion) + unsatisfied_criterion_items = [ + item + for item in self.state.criteria.items() + if not self._is_current_pin_satisfying(*item) ] # All criteria are accounted for. Nothing more to pin, we are done! - if not unsatisfied_names: - self._r.ending(state=self.state) + if not unsatisfied_criterion_items: + self._r.ending(self.state) return self.state # Choose the most preferred unpinned criterion to try. - name = min(unsatisfied_names, key=self._get_preference) - failure_causes = self._attempt_to_pin_criterion(name) + name, criterion = min( + unsatisfied_criterion_items, + key=self._get_criterion_item_preference, + ) + failure_causes = self._attempt_to_pin_criterion(name, criterion) 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 # an unpinned state, so we can work on it in the next round. - self._r.resolving_conflicts(causes=causes) success = self._backtrack() - self.state.backtrack_causes[:] = causes # Dead ends everywhere. Give up. if not success: - raise ResolutionImpossible(self.state.backtrack_causes) + causes = [i for c in failure_causes for i in c.information] + raise ResolutionImpossible(causes) else: # Pinning was successful. Push a new state to do another pin. self._push_new_state() - self._r.ending_round(index=round_index, state=self.state) + self._r.ending_round(round_index, self.state) raise ResolutionTooDeep(max_rounds) diff -Nru python-resolvelib-0.8.1/src/resolvelib/resolvers.pyi python-resolvelib-0.5.4/src/resolvelib/resolvers.pyi --- python-resolvelib-0.8.1/src/resolvelib/resolvers.pyi 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/resolvers.pyi 1970-01-01 00:00:00.000000000 +0000 @@ -1,67 +0,0 @@ -from typing import ( - Collection, - Generic, - Iterable, - Iterator, - List, - Mapping, - Optional, -) - -from .providers import AbstractProvider, AbstractResolver -from .structs import CT, KT, RT, DirectedGraph, IterableView - -# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. -# https://stackoverflow.com/a/50531189/1376863 -class RequirementInformation(tuple, Generic[RT, CT]): - requirement: RT - parent: Optional[CT] - -class Criterion(Generic[RT, CT, KT]): - candidates: IterableView[CT] - information: Collection[RequirementInformation[RT, CT]] - incompatibilities: List[CT] - @classmethod - def from_requirement( - cls, - provider: AbstractProvider[RT, CT, KT], - requirement: RT, - parent: Optional[CT], - ) -> Criterion[RT, CT, KT]: ... - def iter_requirement(self) -> Iterator[RT]: ... - def iter_parent(self) -> Iterator[Optional[CT]]: ... - def merged_with( - self, - provider: AbstractProvider[RT, CT, KT], - requirement: RT, - parent: Optional[CT], - ) -> Criterion[RT, CT, KT]: ... - def excluded_of(self, candidates: List[CT]) -> Criterion[RT, CT, KT]: ... - -class ResolverException(Exception): ... - -class RequirementsConflicted(ResolverException, Generic[RT, CT, KT]): - criterion: Criterion[RT, CT, KT] - -class ResolutionError(ResolverException): ... - -class InconsistentCandidate(ResolverException, Generic[RT, CT, KT]): - candidate: CT - criterion: Criterion[RT, CT, KT] - -class ResolutionImpossible(ResolutionError, Generic[RT, CT]): - causes: List[RequirementInformation[RT, CT]] - -class ResolutionTooDeep(ResolutionError): - round_count: int - -class Result(Generic[RT, CT, KT]): - mapping: Mapping[KT, CT] - graph: DirectedGraph[Optional[KT]] - criteria: Mapping[KT, Criterion[RT, CT, KT]] - -class Resolver(AbstractResolver, Generic[RT, CT, KT]): - base_exception = ResolverException - def resolve( - self, requirements: Iterable[RT], max_rounds: int = 100 - ) -> Result[RT, CT, KT]: ... diff -Nru python-resolvelib-0.8.1/src/resolvelib/structs.py python-resolvelib-0.5.4/src/resolvelib/structs.py --- python-resolvelib-0.8.1/src/resolvelib/structs.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/structs.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,5 +1,3 @@ -import itertools - from .compat import collections_abc @@ -69,43 +67,6 @@ return iter(self._backwards[key]) -class IteratorMapping(collections_abc.Mapping): - def __init__(self, mapping, accessor, appends=None): - self._mapping = mapping - self._accessor = accessor - self._appends = appends or {} - - def __repr__(self): - return "IteratorMapping({!r}, {!r}, {!r})".format( - self._mapping, - self._accessor, - self._appends, - ) - - def __bool__(self): - return bool(self._mapping or self._appends) - - __nonzero__ = __bool__ # XXX: Python 2. - - def __contains__(self, key): - return key in self._mapping or key in self._appends - - def __getitem__(self, k): - try: - v = self._mapping[k] - except KeyError: - return iter(self._appends[k]) - return itertools.chain(self._accessor(v), self._appends.get(k, ())) - - def __iter__(self): - more = (k for k in self._appends if k not in self._mapping) - return itertools.chain(self._mapping, more) - - def __len__(self): - more = sum(1 for k in self._appends if k not in self._mapping) - return len(self._mapping) + more - - class _FactoryIterableView(object): """Wrap an iterator factory returned by `find_matches()`. @@ -133,6 +94,18 @@ def __iter__(self): return self._factory() + def for_preference(self): + """Provide an candidate iterable for `get_preference()`""" + return self._factory() + + def excluding(self, candidates): + """Create a new instance excluding specified candidates.""" + + def factory(): + return (c for c in self._factory() if c not in candidates) + + return type(self)(factory) + class _SequenceIterableView(object): """Wrap an iterable returned by find_matches(). @@ -155,6 +128,17 @@ def __iter__(self): return iter(self._sequence) + def __len__(self): + return len(self._sequence) + + def for_preference(self): + """Provide an candidate iterable for `get_preference()`""" + return self._sequence + + def excluding(self, candidates): + """Create a new instance excluding specified candidates.""" + return type(self)([c for c in self._sequence if c not in candidates]) + def build_iter_view(matches): """Build an iterable view from the value returned by `find_matches()`.""" diff -Nru python-resolvelib-0.8.1/src/resolvelib/structs.pyi python-resolvelib-0.5.4/src/resolvelib/structs.pyi --- python-resolvelib-0.8.1/src/resolvelib/structs.pyi 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/src/resolvelib/structs.pyi 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -from abc import ABCMeta -from typing import ( - Callable, - Container, - Generic, - Iterable, - Iterator, - Mapping, - Tuple, - TypeVar, - Union, -) - -KT = TypeVar("KT") # Identifier. -RT = TypeVar("RT") # Requirement. -CT = TypeVar("CT") # Candidate. -_T = TypeVar("_T") - -Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]] - -class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta): - pass - -class IterableView(Container[CT], Iterable[CT], metaclass=ABCMeta): - pass - -class DirectedGraph(Generic[KT]): - def __iter__(self) -> Iterator[KT]: ... - def __len__(self) -> int: ... - def __contains__(self, key: KT) -> bool: ... - def copy(self) -> "DirectedGraph[KT]": ... - def add(self, key: KT) -> None: ... - def remove(self, key: KT) -> None: ... - def connected(self, f: KT, t: KT) -> bool: ... - def connect(self, f: KT, t: KT) -> None: ... - def iter_edges(self) -> Iterable[Tuple[KT, KT]]: ... - def iter_children(self, key: KT) -> Iterable[KT]: ... - def iter_parents(self, key: KT) -> Iterable[KT]: ... - -def build_iter_view(matches: Matches) -> IterableView[CT]: ... diff -Nru python-resolvelib-0.8.1/tests/conftest.py python-resolvelib-0.5.4/tests/conftest.py --- python-resolvelib-0.8.1/tests/conftest.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/conftest.py 2020-12-27 12:17:14.000000000 +0000 @@ -1,29 +1,8 @@ -from __future__ import print_function - import pytest -from resolvelib import BaseReporter - - -class TestReporter(BaseReporter): - def __init__(self): - self._indent = 0 - - def backtracking(self, candidate): - self._indent -= 1 - assert self._indent >= 0 - print(" " * self._indent, "Back ", candidate, sep="") - - def pinning(self, candidate): - print(" " * self._indent, "Pin ", candidate, sep="") - self._indent += 1 +from resolvelib.reporters import BaseReporter @pytest.fixture(scope="session") -def reporter_cls(): - return TestReporter - - -@pytest.fixture() -def reporter(reporter_cls): - return reporter_cls() +def base_reporter(): + return BaseReporter() diff -Nru python-resolvelib-0.8.1/tests/functional/cocoapods/test_resolvers_cocoapods.py python-resolvelib-0.5.4/tests/functional/cocoapods/test_resolvers_cocoapods.py --- python-resolvelib-0.8.1/tests/functional/cocoapods/test_resolvers_cocoapods.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/functional/cocoapods/test_resolvers_cocoapods.py 2020-12-27 12:17:14.000000000 +0000 @@ -5,13 +5,14 @@ import re import string -import commentjson # type: ignore +import commentjson import packaging.specifiers import packaging.version import pytest from resolvelib import AbstractProvider, ResolutionImpossible, Resolver + Requirement = collections.namedtuple("Requirement", "name spec") Candidate = collections.namedtuple("Candidate", "name ver deps") @@ -104,27 +105,17 @@ def identify(self, requirement_or_candidate): return requirement_or_candidate.name - def get_preference( - self, - identifier, - resolutions, - candidates, - information, - backtrack_causes, - ): - return sum(1 for _ in candidates[identifier]) + def get_preference(self, resolution, candidates, information): + return len(candidates) - def _iter_matches(self, name, requirements, incompatibilities): + def _iter_matches(self, name, requirements): try: data = self.index[name] except KeyError: 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]): - continue - if version in bad_versions: + if any(version not in r.spec for r in requirements): continue # Some fixtures incorrectly set dependencies to an empty list. dependencies = entry["dependencies"] or {} @@ -134,14 +125,14 @@ ] yield Candidate(entry["name"], version, dependencies) - def find_matches(self, identifier, requirements, incompatibilities): - + def find_matches(self, requirements): + name = requirements[0].name candidates = sorted( - self._iter_matches(identifier, requirements, incompatibilities), + self._iter_matches(name, requirements), key=operator.attrgetter("ver"), reverse=True, ) - pinned = self.pinned_versions.get(identifier) + pinned = self.pinned_versions.get(name) for c in candidates: if pinned is not None and c.ver != pinned: continue diff -Nru python-resolvelib-0.8.1/tests/functional/conftest.py python-resolvelib-0.5.4/tests/functional/conftest.py --- python-resolvelib-0.8.1/tests/functional/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-resolvelib-0.5.4/tests/functional/conftest.py 2020-12-27 12:17:14.000000000 +0000 @@ -0,0 +1,24 @@ +from __future__ import print_function + +import pytest + +from resolvelib import BaseReporter + + +class TestReporter(BaseReporter): + def __init__(self): + self._indent = 0 + + def backtracking(self, candidate): + self._indent -= 1 + assert self._indent >= 0 + print(" " * self._indent, "Back ", candidate, sep="") + + def pinning(self, candidate): + print(" " * self._indent, "Pin ", candidate, sep="") + self._indent += 1 + + +@pytest.fixture() +def reporter(): + return TestReporter() diff -Nru python-resolvelib-0.8.1/tests/functional/python/py2index.py python-resolvelib-0.5.4/tests/functional/python/py2index.py --- python-resolvelib-0.8.1/tests/functional/python/py2index.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/functional/python/py2index.py 2020-12-27 12:17:14.000000000 +0000 @@ -18,14 +18,21 @@ import os import pathlib import re -import sys import urllib.parse +import sys import zipfile + +import html5lib +import packaging.requirements +import packaging.tags +import packaging.utils +import packaging.version +import requests + from typing import ( - IO, BinaryIO, Dict, - FrozenSet, + IO, Iterable, Iterator, List, @@ -37,12 +44,6 @@ cast, ) -import html5lib -import packaging.requirements -import packaging.tags -import packaging.utils -import packaging.version -import requests logger = logging.getLogger() @@ -113,7 +114,7 @@ return path -def _parse_tag(s: str) -> FrozenSet[packaging.tags.Tag]: +def _parse_tag(s: str) -> Set[packaging.tags.Tag]: try: return packaging.tags.parse_tag(s) except ValueError: @@ -122,7 +123,7 @@ @dataclasses.dataclass() class WheelMatcher: - required_python: packaging.version.Version + required_python: Optional[packaging.version.Version] tags: Dict[packaging.tags.Tag, int] @classmethod @@ -132,7 +133,7 @@ impl: Optional[str], plats: Optional[List[str]], ) -> WheelMatcher: - required_python = packaging.version.Version( + required_python = packaging.version.parse( ".".join(str(v) for v in python_version) ) # TODO: Add ABI customization. @@ -277,7 +278,7 @@ dep, ) return None - more.add(str(packaging.utils.canonicalize_name(req.name))) + more.add(packaging.utils.canonicalize_name(req.name)) return more def find(self, package_names: Iterable[str]) -> dict: diff -Nru python-resolvelib-0.8.1/tests/functional/python/test_resolvers_python.py python-resolvelib-0.5.4/tests/functional/python/test_resolvers_python.py --- python-resolvelib-0.8.1/tests/functional/python/test_resolvers_python.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/functional/python/test_resolvers_python.py 2020-12-27 12:17:14.000000000 +0000 @@ -14,6 +14,7 @@ from resolvelib import AbstractProvider, ResolutionImpossible, Resolver + Candidate = collections.namedtuple("Candidate", "name version extras") @@ -49,6 +50,7 @@ packaging.requirements.Requirement(r) for r in case_data["requested"] ] + self.pinned_versions = {} if "resolved" in case_data: self.expected_resolution = { @@ -71,36 +73,35 @@ return "{}[{}]".format(name, extras_str) return name - def get_preference( - self, - identifier, - resolutions, - candidates, - information, - backtrack_causes, - ): - transitive = all(p is not None for _, p in information[identifier]) - return (transitive, identifier) - - def _iter_matches(self, identifier, requirements, incompatibilities): - name, _, _ = identifier.partition("[") - bad_versions = {c.version for c in incompatibilities[identifier]} - extras = {e for r in requirements[identifier] for e in r.extras} - for key in self.index[name]: - v = packaging.version.parse(key) - if any(v not in r.specifier for r in requirements[identifier]): - continue - if v in bad_versions: + def get_preference(self, resolution, candidates, information): + transitive = all(parent is not None for _, parent in information) + key = next(iter(candidates)).name if candidates else "" + return (transitive, key) + + def _iter_matches(self, name, requirements): + extras = {e for r in requirements for e in r.extras} + for key, value in self.index[name].items(): + version = packaging.version.parse(key) + if any(version not in r.specifier for r in requirements): continue - yield Candidate(name=name, version=v, extras=extras) + yield Candidate( + name=name, + version=version, + extras=extras, + ) - def find_matches(self, identifier, requirements, incompatibilities): + def find_matches(self, requirements): + name = packaging.utils.canonicalize_name(requirements[0].name) candidates = sorted( - self._iter_matches(identifier, requirements, incompatibilities), + (c for c in self._iter_matches(name, requirements)), key=operator.attrgetter("version"), reverse=True, ) - return candidates + pinned = self.pinned_versions.get(name) + for candidate in candidates: + if pinned is not None and pinned != candidate.version: + continue + yield candidate def is_satisfied_by(self, requirement, candidate): return candidate.version in requirement.specifier diff -Nru python-resolvelib-0.8.1/tests/functional/swift-package-manager/test_resolvers_swift.py python-resolvelib-0.5.4/tests/functional/swift-package-manager/test_resolvers_swift.py --- python-resolvelib-0.8.1/tests/functional/swift-package-manager/test_resolvers_swift.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/functional/swift-package-manager/test_resolvers_swift.py 2020-12-27 12:17:14.000000000 +0000 @@ -8,6 +8,7 @@ from resolvelib.providers import AbstractProvider from resolvelib.resolvers import Resolver + Requirement = collections.namedtuple("Requirement", "container constraint") Candidate = collections.namedtuple("Candidate", "container version") @@ -78,35 +79,25 @@ def identify(self, requirement_or_candidate): return requirement_or_candidate.container["identifier"] - def get_preference( - self, - identifier, - resolutions, - candidates, - information, - backtrack_causes, - ): - return sum(1 for _ in candidates[identifier]) - - def _iter_matches(self, identifier, requirements, incompatibilities): - bad_versions = {c.version for c in incompatibilities[identifier]} - container = next(requirements[identifier]).container + def get_preference(self, resolution, candidates, information): + return len(candidates) + + def _iter_matches(self, requirements): + container = requirements[0].container for version in container["versions"]: - if version in bad_versions: - continue ver = _parse_version(version) satisfied = all( _is_version_allowed(ver, r.constraint["requirement"]) - for r in requirements[identifier] + for r in requirements ) if not satisfied: continue preference = _calculate_preference(ver) yield (preference, Candidate(container, version)) - def find_matches(self, identifier, requirements, incompatibilities): + def find_matches(self, requirements): matches = sorted( - self._iter_matches(identifier, requirements, incompatibilities), + self._iter_matches(requirements), key=operator.itemgetter(0), reverse=True, ) @@ -141,8 +132,8 @@ return SwiftInputProvider(request.param) -def test_resolver(provider, reporter): - resolver = Resolver(provider, reporter) +def test_resolver(provider, base_reporter): + resolver = Resolver(provider, base_reporter) result = resolver.resolve(provider.root_requirements) display = { diff -Nru python-resolvelib-0.8.1/tests/test_resolvers.py python-resolvelib-0.5.4/tests/test_resolvers.py --- python-resolvelib-0.8.1/tests/test_resolvers.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/test_resolvers.py 2020-12-27 12:17:14.000000000 +0000 @@ -4,7 +4,6 @@ AbstractProvider, BaseReporter, InconsistentCandidate, - ResolutionImpossible, Resolver, ) @@ -14,31 +13,26 @@ candidate = "bar" class Provider(AbstractProvider): - def __init__(self, requirement, candidate): - self.requirement = requirement - self.candidate = candidate - - def identify(self, requirement_or_candidate): - assert requirement_or_candidate is self.requirement - return requirement_or_candidate + def identify(self, d): + assert d is requirement or d is candidate + return d - def get_preference(self, **_): + def get_preference(self, *_): return 0 - def get_dependencies(self, **_): + def get_dependencies(self, _): return [] - def find_matches(self, identifier, requirements, incompatibilities): - assert list(requirements[identifier]) == [self.requirement] - assert next(incompatibilities[identifier], None) is None - return [self.candidate] - - def is_satisfied_by(self, requirement, candidate): - assert requirement is self.requirement - assert candidate is self.candidate + def find_matches(self, rs): + assert len(rs) == 1 and rs[0] is requirement + return [candidate] + + def is_satisfied_by(self, r, c): + assert r is requirement + assert c is candidate return False - resolver = Resolver(Provider(requirement, candidate), BaseReporter()) + resolver = Resolver(Provider(), BaseReporter()) with pytest.raises(InconsistentCandidate) as ctx: resolver.resolve([requirement]) @@ -46,100 +40,3 @@ assert str(ctx.value) == "Provided candidate 'bar' does not satisfy 'foo'" assert ctx.value.candidate is candidate assert list(ctx.value.criterion.iter_requirement()) == [requirement] - - -@pytest.mark.parametrize("specifiers", [["1", "12"], ["12", "1"]]) -def test_candidate_depends_on_requirements_of_same_identifier(specifiers): - # This test ensures if a candidate has multiple dependencies under the same - # identifier, all dependencies of that identifier are correctly pulled in. - # The parametrization ensures both requirement ordering work. - - # Parent depends on child twice, one allows v2, the other does not. - # Each candidate is a 3-tuple (name, version, dependencies). - # Each requirement is a 2-tuple (name, allowed_versions). - # Candidate v2 is in from so it is preferred when both are allowed. - all_candidates = { - "parent": [("parent", "1", [("child", s) for s in specifiers])], - "child": [("child", "2", []), ("child", "1", [])], - } - - class Provider(AbstractProvider): - def identify(self, requirement_or_candidate): - return requirement_or_candidate[0] - - def get_preference(self, **_): - return 0 - - def get_dependencies(self, candidate): - return candidate[2] - - def find_matches(self, identifier, requirements, incompatibilities): - assert not list(incompatibilities[identifier]) - return ( - candidate - for candidate in all_candidates[identifier] - if all(candidate[1] in r[1] for r in requirements[identifier]) - ) - - def is_satisfied_by(self, requirement, candidate): - return candidate[1] in requirement[1] - - # Now when resolved, both requirements to child specified by parent should - # be pulled, and the resolver should choose v1, not v2 (happens if the - # v1-only requirement is dropped). - resolver = Resolver(Provider(), BaseReporter()) - result = resolver.resolve([("parent", {"1"})]) - - assert set(result.mapping) == {"parent", "child"} - assert result.mapping["child"] == ("child", "1", []) - - -def test_resolving_conflicts(): - all_candidates = { - "a": [("a", 1, [("q", {1})]), ("a", 2, [("q", {2})])], - "b": [("b", 1, [("q", {1})])], - "q": [("q", 1, []), ("q", 2, [])], - } - - class Reporter(BaseReporter): - def __init__(self): - self.backtracking_causes = None - - def resolving_conflicts(self, causes): - self.backtracking_causes = causes - - class Provider(AbstractProvider): - def identify(self, requirement_or_candidate): - return requirement_or_candidate[0] - - def get_preference(self, **_): - return 0 - - def get_dependencies(self, candidate): - return candidate[2] - - def find_matches(self, identifier, requirements, incompatibilities): - bad_versions = {c[1] 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 - ] - return sorted(candidates, key=lambda c: c[1], reverse=True) - - def is_satisfied_by(self, requirement, candidate): - return candidate[1] in requirement[1] - - def run_resolver(*args): - reporter = Reporter() - resolver = Resolver(Provider(), reporter) - try: - resolver.resolve(*args) - return reporter.backtracking_causes - except ResolutionImpossible as e: - return e.causes - - backtracking_causes = run_resolver([("a", {1, 2}), ("b", {1})]) - exception_causes = run_resolver([("a", {2}), ("b", {1})]) - assert exception_causes == backtracking_causes diff -Nru python-resolvelib-0.8.1/tests/test_structs.py python-resolvelib-0.5.4/tests/test_structs.py --- python-resolvelib-0.8.1/tests/test_structs.py 2021-10-11 21:05:57.000000000 +0000 +++ python-resolvelib-0.5.4/tests/test_structs.py 2020-12-27 12:17:14.000000000 +0000 @@ -50,3 +50,39 @@ next(iterator_a) assert next(iterator_b) == 0 assert next(iterator_a) == 1 + + +@pytest.mark.parametrize("source", [_generate]) +def test_iter_view_for_preference_based_on_factory(source): + """Factory-based view returns an iterator for preference.""" + view = build_iter_view(source) + iterator = view.for_preference() + assert iter(iterator) is iterator, "not an iterator" + assert list(iterator) == [0, 1] + + +@pytest.mark.parametrize("source", [[0, 1], iter([0, 1])]) +def test_iter_view_for_preference_based_on_sequence(source): + """Sequence-based view returns a sequence for preference.""" + view = build_iter_view(source) + assert view.for_preference() == [0, 1] + + +@pytest.mark.parametrize( + "source", + [lambda: _generate, lambda: [0, 1], _generate], + ids=["callable", "sequence", "iterator"], +) +@pytest.mark.parametrize( + "exclusion, expected", + [ + ([1], [0]), + ([0, 1], []), + ([1, 2], [0]), + ([2, 3], [0, 1]), + ], + ids=["one", "all", "partial", "none"], +) +def test_itera_view_excluding(source, exclusion, expected): + view = build_iter_view(source()) + assert list(view.excluding(exclusion)) == expected diff -Nru python-resolvelib-0.8.1/.travis.yml python-resolvelib-0.5.4/.travis.yml --- python-resolvelib-0.8.1/.travis.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-resolvelib-0.5.4/.travis.yml 2020-12-27 12:17:14.000000000 +0000 @@ -0,0 +1,31 @@ +language: python + +cache: pip + +jobs: + fast_finish: true + include: + - stage: lint + name: syntax + install: pip install black flake8 + script: + - black --check . + - flake8 . + - name: packaging + install: pip install .[release] + script: setl publish --no-upload + - stage: primary + - python: "2.7" + - stage: secondary + python: "3.8" + - python: "3.7" + - python: "3.6" + - python: "nightly" + allow_failures: + - python: "nightly" + +python: "3.9-dev" + +install: pip install .[test] + +script: pytest tests