Repository: pypa/pip

Issue:
New resolver cannot installs distributions that only have pre releases
**Environment**

* pip version: master, today
* Python version: 3
* OS: linux

**Description**

I want to install a distribution that only has pre-releases. The legacy resolver does support this. The new one does not. 

Note: using `--pre` does not seem to influence the result. The legacy resolver could install such distributions without using `--pre`.

**Expected behavior**

Installation should succeed.

**How to Reproduce**

```console
$ pip install --no-deps odoo13-addon-date-range --unstable-feature=resolver
ERROR: Exception:
Traceback (most recent call last):
  File "/home/me/pip/src/pip/_internal/cli/base_command.py", line 199, in _main
    status = self.run(options, args)
  File "/home/me/pip/src/pip/_internal/cli/req_command.py", line 185, in wrapper
    return func(self, options, args)
  File "/home/me/pip/src/pip/_internal/commands/install.py", line 333, in run
    reqs, check_supported_wheels=not options.target_dir
  File "/home/me/pip/src/pip/_internal/resolution/resolvelib/resolver.py", line 80, in resolve
    self._result = resolver.resolve(requirements)
  File "/home/me/pip/src/pip/_vendor/resolvelib/resolvers.py", line 413, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
  File "/home/me/pip/src/pip/_vendor/resolvelib/resolvers.py", line 310, in resolve
    failure_causes = self._attempt_to_pin_criterion(name, criterion)
  File "/home/me/pip/src/pip/_vendor/resolvelib/resolvers.py", line 240, in _attempt_to_pin_criterion
    raise InconsistentCandidate(candidate, criterion)
pip._vendor.resolvelib.resolvers.InconsistentCandidate: Provided candidate LinkCandidate('https://files.pythonhosted.org/packages/1f/0b/945335a37082b6b013cc1331f49e3f5b6a18cdd0b693475e6ca9e9a7df6e/odoo13_addon_date_range-13.0.1.0.1.dev8-py3-none-any.whl#sha256=3883bbe87db8d5db4364e8a42e86546e19e8e4f123d98c4e9454587dfa9401df (from https://pypi.org/simple/odoo13-addon-date-range/) (requires-python:>=3.5)') does not satisfy SpecifierRequirement('odoo13-addon-date-range')
```

Note I used `--no-deps` because a dependency is not on pypi, but that has no influence on the result.


Discussion:
`InconsistentCandidate` is raised if a match from `find_matches()` does not return true for `is_satisfied_by()`. So my guess is we need to fix `is_satisfied_by()` to accept prereleases.
Nice catch @sbidoul! ^>^

I don't think this needs to be fixed *right now* prior to pip 20.1's beta, but yes, we do have to implement this eventually. :)
It would also fail with `--pre` if the selected version is a prerelease. Not all prereleases fail though, only if the specifier does not contain a prerelease. I think I have a fix.

Resolving patch:
diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py
--- a/src/pip/_internal/resolution/resolvelib/requirements.py
+++ b/src/pip/_internal/resolution/resolvelib/requirements.py
@@ -77,7 +77,11 @@ def is_satisfied_by(self, candidate):
         assert candidate.name == self.name, \
             "Internal issue: Candidate is not for this requirement " \
             " {} vs {}".format(candidate.name, self.name)
-        return candidate.version in self._ireq.req.specifier
+        # We can safely always allow prereleases here since PackageFinder
+        # already implements the prerelease logic, and would have filtered out
+        # prerelease candidates if the user does not expect them.
+        spec = self._ireq.req.specifier
+        return spec.contains(candidate.version, prereleases=True)
 
 
 class RequiresPythonRequirement(Requirement):
@@ -109,4 +113,7 @@ def find_matches(self):
     def is_satisfied_by(self, candidate):
         # type: (Candidate) -> bool
         assert candidate.name == self._candidate.name, "Not Python candidate"
-        return candidate.version in self.specifier
+        # We can safely always allow prereleases here since PackageFinder
+        # already implements the prerelease logic, and would have filtered out
+        # prerelease candidates if the user does not expect them.
+        return self.specifier.contains(candidate.version, prereleases=True)


Test patch:
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -498,3 +498,34 @@ def test_new_resolver_force_reinstall(script):
         "base not reinstalled"
     )
     assert_installed(script, base="0.1.0")
+
+
+@pytest.mark.parametrize(
+    "available_versions, pip_args, expected_version",
+    [
+        # Choose the latest non-prerelease by default.
+        (["1.0", "2.0a1"], ["pkg"], "1.0"),
+        # Choose the prerelease if the specifier spells out a prerelease.
+        (["1.0", "2.0a1"], ["pkg==2.0a1"], "2.0a1"),
+        # Choose the prerelease if explicitly allowed by the user.
+        (["1.0", "2.0a1"], ["pkg", "--pre"], "2.0a1"),
+        # Choose the prerelease if no stable releases are available.
+        (["2.0a1"], ["pkg"], "2.0a1"),
+    ],
+    ids=["default", "exact-pre", "explicit-pre", "no-stable"],
+)
+def test_new_resolver_handles_prerelease(
+    script,
+    available_versions,
+    pip_args,
+    expected_version,
+):
+    for version in available_versions:
+        create_basic_wheel_for_package(script, "pkg", version)
+    script.pip(
+        "install", "--unstable-feature=resolver",
+        "--no-cache-dir", "--no-index",
+        "--find-links", script.scratch_path,
+        *pip_args
+    )
+    assert_installed(script, pkg=expected_version)