Repository: pypa/pip

Issue:
New resolver: Try == specifiers first to catch simple incompatibilities faster
Idea I have while working on https://github.com/pypa/pip/issues/9011#issuecomment-735968619

This input:

```
tensorboard==2.2.2
torch==1.7.0
tqdm>=4.54

nltk==3.5.0
transformers==3.5.0
tokenizers==0.9.4
jsonlines>=1.2
sacrebleu>=1.4

pytest>=6.1
pytest-cov>=2.10
codecov>=2.1
flake8>=3.8
black==20.8b1
```

Leads to this eventual error:

```
The conflict is caused by:
    The user requested tokenizers==0.9.4
    transformers 3.5.0 depends on tokenizers==0.9.3
```

But the resolver needs a very long time to reach it, because… the packages started with `to` and `tr` are checked last, after the resolver successfully worked out all the other complex open specifier combinations 🤦 

I think we can make things a bit better by checking for `==` (and `===`) specifiers and do those packages first. This is similar to how we previously “optimised” the process by using `len(candidates)` (the less candidates a package has, the more likely it causes conflicts, so we check them first to catch simple cases early), but without needing to actually fetch the list of candidates eagerly.


Discussion:
I like it. Let's do this. :)

Resolving patch:
diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py
--- a/src/pip/_internal/resolution/resolvelib/provider.py
+++ b/src/pip/_internal/resolution/resolvelib/provider.py
@@ -56,9 +56,58 @@ def get_preference(
         information  # type: Sequence[Tuple[Requirement, Candidate]]
     ):
         # type: (...) -> Any
+        """Produce a sort key for given requirement based on preference.
+
+        The lower the return value is, the more preferred this group of
+        arguments is.
+
+        Currently pip considers the followings in order:
+
+        * Prefer if any of the known requirements points to an explicit URL.
+        * If equal, prefer if any requirements contain ``===`` and ``==``.
+        * If equal, prefer if requirements include version constraints, e.g.
+          ``>=`` and ``<``.
+        * If equal, prefer user-specified (non-transitive) requirements.
+        * If equal, order alphabetically for consistency (helps debuggability).
+        """
+
+        def _get_restrictive_rating(requirements):
+            # type: (Iterable[Requirement]) -> int
+            """Rate how restrictive a set of requirements are.
+
+            ``Requirement.get_candidate_lookup()`` returns a 2-tuple for
+            lookup. The first element is ``Optional[Candidate]`` and the
+            second ``Optional[InstallRequirement]``.
+
+            * If the requirement is an explicit one, the explicitly-required
+              candidate is returned as the first element.
+            * If the requirement is based on a PEP 508 specifier, the backing
+              ``InstallRequirement`` is returned as the second element.
+
+            We use the first element to check whether there is an explicit
+            requirement, and the second for equality operator.
+            """
+            lookups = (r.get_candidate_lookup() for r in requirements)
+            cands, ireqs = zip(*lookups)
+            if any(cand is not None for cand in cands):
+                return 0
+            spec_sets = (ireq.specifier for ireq in ireqs if ireq)
+            operators = [
+                specifier.operator
+                for spec_set in spec_sets
+                for specifier in spec_set
+            ]
+            if any(op in ("==", "===") for op in operators):
+                return 1
+            if operators:
+                return 2
+            # A "bare" requirement without any version requirements.
+            return 3
+
+        restrictive = _get_restrictive_rating(req for req, _ in information)
         transitive = all(parent is not None for _, parent in information)
         key = next(iter(candidates)).name if candidates else ""
-        return (transitive, key)
+        return (restrictive, transitive, key)
 
     def find_matches(self, requirements):
         # type: (Sequence[Requirement]) -> Iterable[Candidate]


Test patch: