Repository: pypa/pip

Issue:
Resolve/install user-requested packages in the same order the user requests them
**What's the problem this feature will solve?**
Currently, the new resolver orders resolution and installation in alphabetical ordering when all other parameters are equal. While this is technically fine, the user sometimes may expect things to happen in the same order they specify them, e.g.

```
pip install flask django
```

The two packages are entirely unrelated, and currently the new resolver would resolve `django` before `flask`. After resolution, `django` will also be installed before `flask`, since the two packages have no common dependencies, and neither depends on the other. This may be surprising to the user, and disables some “interesting hacks” possible in the legacy resolver such as https://github.com/pypa/pip/issues/9314#issuecomment-748247200.

**Describe the solution you'd like**
Add one more component in both `Provider.get_preference()` and `_req_set_item_sorter()` before the final tie-breaker, to sort the resolution and installation order in the same order the user requests a package.

https://github.com/pypa/pip/blob/89d50dd833c21073a6080311ec0f3e4a3c0878b8/src/pip/_internal/resolution/resolvelib/provider.py#L62-L131

https://github.com/pypa/pip/blob/89d50dd833c21073a6080311ec0f3e4a3c0878b8/src/pip/_internal/resolution/resolvelib/resolver.py#L284-L295

**Alternative Solutions**
Do nothing. We never guarenteed this would work, and the current implementation is perfectly reasonable. But it’s constant overhead to respond to user requests, and this is not nearly as bad as [XKCD-1172](https://xkcd.com/1172/).

**Additional context**
Note that ordering of transitive dependencies is not affected here. They will continue to be sorted alphabetically, and resolved after / installed before user-requested requirements. We cannot break these.



Discussion:
Would we make the ordering a guarantee if we did this? In other words, are we moving from "unspecified, but in practice alphabetical", to "as specified by the user" or to "unspecified, but in practice as specified by the user"?

I'm a little concerned that we might limit future changes if we make this a hard guarantee. Also, it could be hard to word such a guarantee clearly, to make the point that it's only the final tie-break, and other considerations apply first.

Basically, I'm fine with doing this, but reluctant to make it a guarantee, or add tests that enforce it.
I'm ambivalent. I can see benefits of sticking to status quo as well as to doing this.

Unless someone is significantly motivated that this would be a significant reduction in user "pain" or user support overhead for us, I'm inclined to stick with status quo. Like Paul, I'm also a little reluctant to make this a guarantee, and am much more conformable with "yea, it's alphabetical" approach to it, since it's... less code for us. :)

I’m leaning toward “unspecified, but in practice as specified by the user” at least for now. Ordering by user specification is the most intuitive thing to do (from the user’s perspective), and maybe someday someone would convince everyone to make this a guarentee, like how Python managed the dict-being-ordered situation. But before that, this is purely an implementation detail that happens to meet users’ expectation.

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
@@ -5,7 +5,7 @@
 from .base import Constraint
 
 if MYPY_CHECK_RUNNING:
-    from typing import Any, Dict, Iterable, Optional, Sequence, Set, Tuple, Union
+    from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union
 
     from .base import Candidate, Requirement
     from .factory import Factory
@@ -46,7 +46,7 @@ def __init__(
         constraints,  # type: Dict[str, Constraint]
         ignore_dependencies,  # type: bool
         upgrade_strategy,  # type: str
-        user_requested,  # type: Set[str]
+        user_requested,  # type: Dict[str, int]
     ):
         # type: (...) -> None
         self._factory = factory
@@ -77,7 +77,8 @@ def get_preference(
         * 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, prefer user-specified (non-transitive) requirements, and
+          order user-specified requirements by the order they are specified.
         * If equal, order alphabetically for consistency (helps debuggability).
         """
 
@@ -115,8 +116,8 @@ def _get_restrictive_rating(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 ""
+        order = self._user_requested.get(key, float("inf"))
 
         # HACK: Setuptools have a very long and solid backward compatibility
         # track record, and extremely few projects would request a narrow,
@@ -128,7 +129,7 @@ def _get_restrictive_rating(requirements):
         # while we work on "proper" branch pruning techniques.
         delay_this = (key == "setuptools")
 
-        return (delay_this, restrictive, transitive, key)
+        return (delay_this, restrictive, order, key)
 
     def find_matches(self, requirements):
         # type: (Sequence[Requirement]) -> Iterable[Candidate]
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -79,9 +79,9 @@ def resolve(self, root_reqs, check_supported_wheels):
         # type: (List[InstallRequirement], bool) -> RequirementSet
 
         constraints = {}  # type: Dict[str, Constraint]
-        user_requested = set()  # type: Set[str]
+        user_requested = {}  # type: Dict[str, int]
         requirements = []
-        for req in root_reqs:
+        for i, req in enumerate(root_reqs):
             if req.constraint:
                 # Ensure we only accept valid constraints
                 problem = check_invalid_constraint_type(req)
@@ -96,7 +96,9 @@ def resolve(self, root_reqs, check_supported_wheels):
                     constraints[name] = Constraint.from_ireq(req)
             else:
                 if req.user_supplied and req.name:
-                    user_requested.add(canonicalize_name(req.name))
+                    canonical_name = canonicalize_name(req.name)
+                    if canonical_name not in user_requested:
+                        user_requested[canonical_name] = i
                 r = self.factory.make_requirement_from_install_req(
                     req, requested_extras=(),
                 )


Test patch:
diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py
--- a/tests/unit/resolution_resolvelib/conftest.py
+++ b/tests/unit/resolution_resolvelib/conftest.py
@@ -69,5 +69,5 @@ def provider(factory):
         constraints={},
         ignore_dependencies=False,
         upgrade_strategy="to-satisfy-only",
-        user_requested=set(),
+        user_requested={},
     )