Repository: pypa/pip

Issue:
python subprocess invocation to inject setuptools pollutes setuptools argument list
setuptools (and probably distutils) assume the very first argument from sys.argv to be the executed setup file. Invoking a python subprocess with -c to execute a command will set '-c' to be the very first argument to sys.argv (see the docs). This leads to the manifest_maker warning noted at https://github.com/pypa/pip/blob/9b11ed231cc0c7a8101f1ffbdc8b352a12c511b7/pip/req/req_install.py#L868 and also will cause arguments to the setuptools (or distutils) commands to be polluted with an additional '-c', whether it's wanted or not.

As a workaround, manipulate the argument list prior to exec() in https://github.com/pypa/pip/blob/9b11ed231cc0c7a8101f1ffbdc8b352a12c511b7/pip/req/req_install.py#L843 and similar invocations within pip:

```
import sys, setuptools, tokenize
sys.argv[0] = %r; ....
```


Discussion:
bertilhatt: Wasn’t able to test locally because of too many errors after cloning the repository; see https://github.com/pypa/pip/issues/5937.
benoit-pierre: You forgot a number or references to `SETUPTOOLS_SHIM`.
pradyunsg: ```
./src/pip/_internal/download.py:from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
./src/pip/_internal/download.py:    sdist_args.append(SETUPTOOLS_SHIM % setup_py)
./src/pip/_internal/utils/setuptools_build.py:SETUPTOOLS_SHIM = (
./src/pip/_internal/req/req_install.py:from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
./src/pip/_internal/req/req_install.py:            script = SETUPTOOLS_SHIM % self.setup_py
./src/pip/_internal/req/req_install.py:                        SETUPTOOLS_SHIM % self.setup_py
./src/pip/_internal/req/req_install.py:        install_args.append(SETUPTOOLS_SHIM % self.setup_py)
./src/pip/_internal/wheel.py:from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
./src/pip/_internal/wheel.py:            SETUPTOOLS_SHIM % req.setup_py
```

BrownTruck: Hello!

I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the ``master`` branch into this pull request or rebase this pull request against ``master`` then it will be eligible for code review and hopefully merging!
pganssle: @bertilhatt @pypa/pip-committers I have rebased (and did some squashing along the way) this PR against the current master in [this branch on my fork](https://github.com/pganssle/pip/tree/subprocess_invocation_out_setuptools_args)

Could a maintainer force-push my changes to the original PR branch so that this can be merged? Here's how to do that:

```
git remote add bertilhatt git@github.com:bertilhatt/pip.git
git fetch bertilhatt
git checkout subprocess_invocation_out_setuptools_args
git remote add pganssle git@github.com:pganssle/pip.git
git fetch pganssle
git reset --hard pganssle/subprocess_invocation_out_setuptools_args
git push --force-with-lease
```

@bertilhatt If you want to do it yourself, you can skip the first two lines and start at `git checkout subprocess_invocation_out_setuptools_args`.
cjerdonek: @pganssle I did what you requested. Thanks!
pganssle: Ugh, I seem to have broken things in my rebase because I accidentally converted `setup_py` to `setup.py`.

@cjerdonek I've force-pushed a fixed version to my branch that hopefully fixes this. Sorry about that, you should be able to `fetch` my latest version and reset @bertilhatt's to my current head, which is commit [844f4f697f74575213ed786635c17de4fb66f350](https://github.com/pganssle/pip/commit/844f4f697f74575213ed786635c17de4fb66f350)
cjerdonek: Done.
BrownTruck: Hello!

I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the ``master`` branch into this pull request or rebase this pull request against ``master`` then it will be eligible for code review and hopefully merging!
cjerdonek: @pradyunsg @pganssle Could either of you take a look at this again before it's merged? It's been several months and a couple rebases before either of you gave the first okay.
pradyunsg: LGTM. @cjerdonek Let's merge if you think this is good to go. :)

Patch:
diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst
index 96fd1c018..a84f4746d 100644
--- a/docs/html/reference/pip_install.rst
+++ b/docs/html/reference/pip_install.rst
@@ -856,8 +856,8 @@ Examples
 
       $ pip install SomePackage[PDF]
       $ pip install git+https://git.repo/some_pkg.git#egg=SomePackage[PDF]
-      $ pip install .[PDF]  # project in current directory
       $ pip install SomePackage[PDF]==3.0
+      $ pip install -e .[PDF]==3.0  # editable project in current directory
       $ pip install SomePackage[PDF,EPUB]  # multiple extras
 
 
diff --git a/news/1234.trivial b/news/1234.trivial
deleted file mode 100644
index e69de29bb..000000000
diff --git a/news/1890.bugfix b/news/1890.bugfix
new file mode 100644
index 000000000..8f85e1255
--- /dev/null
+++ b/news/1890.bugfix
@@ -0,0 +1,2 @@
+Set ``sys.argv[0]`` to the underlying ``setup.py`` when invoking ``setup.py``
+via the setuptools shim so setuptools doesn't think the path is ``-c``.
diff --git a/news/4733.doc b/news/4733.doc
deleted file mode 100644
index 958736248..000000000
--- a/news/4733.doc
+++ /dev/null
@@ -1,1 +0,0 @@
-Replace a failing example of pip installs with extras with a working one.
diff --git a/news/5671.bugfix b/news/5671.bugfix
deleted file mode 100644
index 1dfa6c394..000000000
--- a/news/5671.bugfix
+++ /dev/null
@@ -1,1 +0,0 @@
-Make the "ascii" progress bar really be "ascii" and not Unicode.
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index e0e78678c..247de0ce4 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -7,6 +7,7 @@ import logging
 import mimetypes
 import os
 import re
+from collections import namedtuple
 
 from pip._vendor import html5lib, requests, six
 from pip._vendor.distlib.compat import unescape
@@ -40,8 +41,8 @@ from pip._internal.wheel import Wheel
 if MYPY_CHECK_RUNNING:
     from logging import Logger
     from typing import (
-        Any, Callable, FrozenSet, Iterable, Iterator, List, MutableMapping,
-        Optional, Sequence, Set, Text, Tuple, Union,
+        Any, Callable, Iterable, Iterator, List, MutableMapping, Optional,
+        Sequence, Set, Text, Tuple, Union,
     )
     import xml.etree.ElementTree
     from pip._vendor.packaging.version import _BaseVersion
@@ -49,7 +50,6 @@ if MYPY_CHECK_RUNNING:
     from pip._internal.models.search_scope import SearchScope
     from pip._internal.req import InstallRequirement
     from pip._internal.download import PipSession
-    from pip._internal.pep425tags import Pep425Tag
 
     BuildTag = Tuple[Any, ...]  # either empty tuple or Tuple[int, str]
     CandidateSortingKey = (
@@ -301,58 +301,62 @@ def _check_link_requires_python(
     return True
 
 
-class LinkEvaluator(object):
+class CandidateEvaluator(object):
 
     """
-    Responsible for evaluating links for a particular project.
+    Responsible for filtering and sorting candidates for installation based
+    on what tags are valid.
     """
 
-    _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
-
     # Don't include an allow_yanked default value to make sure each call
     # site considers whether yanked releases are allowed. This also causes
     # that decision to be made explicit in the calling code, which helps
     # people when reading the code.
     def __init__(
         self,
-        project_name,    # type: str
-        canonical_name,  # type: str
-        formats,         # type: FrozenSet
-        target_python,   # type: TargetPython
-        allow_yanked,    # type: bool
+        allow_yanked,  # type: bool
+        target_python=None,  # type: Optional[TargetPython]
+        prefer_binary=False,   # type: bool
+        allow_all_prereleases=False,  # type: bool
         ignore_requires_python=None,  # type: Optional[bool]
     ):
         # type: (...) -> None
         """
-        :param project_name: The user supplied package name.
-        :param canonical_name: The canonical package name.
-        :param formats: The formats allowed for this package. Should be a set
-            with 'binary' or 'source' or both in it.
-        :param target_python: The target Python interpreter to use when
-            evaluating link compatibility. This is used, for example, to
-            check wheel compatibility, as well as when checking the Python
-            version, e.g. the Python version embedded in a link filename
-            (or egg fragment) and against an HTML link's optional PEP 503
-            "data-requires-python" attribute.
         :param allow_yanked: Whether files marked as yanked (in the sense
             of PEP 592) are permitted to be candidates for install.
+        :param target_python: The target Python interpreter to use to check
+            both the Python version embedded in the filename and the package's
+            "Requires-Python" metadata. If None (the default), then a
+            TargetPython object will be constructed from the running Python.
+        :param allow_all_prereleases: Whether to allow all pre-releases.
         :param ignore_requires_python: Whether to ignore incompatible
-            PEP 503 "data-requires-python" values in HTML links. Defaults
-            to False.
+            "Requires-Python" values in links. Defaults to False.
         """
+        if target_python is None:
+            target_python = TargetPython()
         if ignore_requires_python is None:
             ignore_requires_python = False
 
         self._allow_yanked = allow_yanked
-        self._canonical_name = canonical_name
         self._ignore_requires_python = ignore_requires_python
-        self._formats = formats
+        self._prefer_binary = prefer_binary
         self._target_python = target_python
 
-        self.project_name = project_name
+        # We compile the regex here instead of as a class attribute so as
+        # not to impact pip start-up time.  This is also okay because
+        # CandidateEvaluator is generally instantiated only once per pip
+        # invocation (when PackageFinder is instantiated).
+        self._py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
+
+        self.allow_all_prereleases = allow_all_prereleases
 
-    def evaluate_link(self, link):
-        # type: (Link) -> Tuple[bool, Optional[Text]]
+    def _is_wheel_supported(self, wheel):
+        # type: (Wheel) -> bool
+        valid_tags = self._target_python.get_tags()
+        return wheel.supported(valid_tags)
+
+    def evaluate_link(self, link, search):
+        # type: (Link, Search) -> Tuple[bool, Optional[Text]]
         """
         Determine whether a link is a candidate for installation.
 
@@ -378,8 +382,8 @@ class LinkEvaluator(object):
                 return (False, 'not a file')
             if ext not in SUPPORTED_EXTENSIONS:
                 return (False, 'unsupported archive format: %s' % ext)
-            if "binary" not in self._formats and ext == WHEEL_EXTENSION:
-                reason = 'No binaries permitted for %s' % self.project_name
+            if "binary" not in search.formats and ext == WHEEL_EXTENSION:
+                reason = 'No binaries permitted for %s' % search.supplied
                 return (False, reason)
             if "macosx10" in link.path and ext == '.zip':
                 return (False, 'macosx10 one')
@@ -388,12 +392,11 @@ class LinkEvaluator(object):
                     wheel = Wheel(link.filename)
                 except InvalidWheelFilename:
                     return (False, 'invalid wheel filename')
-                if canonicalize_name(wheel.name) != self._canonical_name:
-                    reason = 'wrong project name (not %s)' % self.project_name
+                if canonicalize_name(wheel.name) != search.canonical:
+                    reason = 'wrong project name (not %s)' % search.supplied
                     return (False, reason)
 
-                supported_tags = self._target_python.get_tags()
-                if not wheel.supported(supported_tags):
+                if not self._is_wheel_supported(wheel):
                     # Include the wheel's tags in the reason string to
                     # simplify troubleshooting compatibility issues.
                     file_tags = wheel.get_formatted_file_tags()
@@ -406,18 +409,16 @@ class LinkEvaluator(object):
 
                 version = wheel.version
 
-        # This should be up by the self.ok_binary check, but see issue 2700.
-        if "source" not in self._formats and ext != WHEEL_EXTENSION:
-            return (False, 'No sources permitted for %s' % self.project_name)
+        # This should be up by the search.ok_binary check, but see issue 2700.
+        if "source" not in search.formats and ext != WHEEL_EXTENSION:
+            return (False, 'No sources permitted for %s' % search.supplied)
 
         if not version:
             version = _extract_version_from_fragment(
-                egg_info, self._canonical_name,
+                egg_info, search.canonical,
             )
         if not version:
-            return (
-                False, 'Missing project version for %s' % self.project_name,
-            )
+            return (False, 'Missing project version for %s' % search.supplied)
 
         match = self._py_version_re.search(version)
         if match:
@@ -439,36 +440,6 @@ class LinkEvaluator(object):
 
         return (True, version)
 
-
-class CandidateEvaluator(object):
-
-    """
-    Responsible for filtering and sorting candidates for installation based
-    on what tags are valid.
-    """
-
-    def __init__(
-        self,
-        supported_tags=None,  # type: Optional[List[Pep425Tag]]
-        prefer_binary=False,  # type: bool
-        allow_all_prereleases=False,  # type: bool
-    ):
-        # type: (...) -> None
-        """
-        :param supported_tags: The PEP 425 tags supported by the target
-            Python in order of preference (most preferred first). If None,
-            then the list will be generated from the running Python.
-        :param allow_all_prereleases: Whether to allow all pre-releases.
-        """
-        if supported_tags is None:
-            target_python = TargetPython()
-            supported_tags = target_python.get_tags()
-
-        self._prefer_binary = prefer_binary
-        self._supported_tags = supported_tags
-
-        self.allow_all_prereleases = allow_all_prereleases
-
     def make_found_candidates(
         self,
         candidates,      # type: List[InstallationCandidate]
@@ -500,17 +471,7 @@ class CandidateEvaluator(object):
                 prereleases=allow_prereleases,
             )
         }
-
-        # Again, converting version to str to deal with debundling.
-        applicable_candidates = [
-            c for c in candidates if str(c.version) in versions
-        ]
-
-        return FoundCandidates(
-            candidates,
-            applicable_candidates=applicable_candidates,
-            evaluator=self,
-        )
+        return FoundCandidates(candidates, versions=versions, evaluator=self)
 
     def _sort_key(self, candidate):
         # type: (InstallationCandidate) -> CandidateSortingKey
@@ -529,7 +490,7 @@ class CandidateEvaluator(object):
         If not finding wheels, they are sorted by version only.
         If finding wheels, then the sort order is by version, then:
           1. existing installs
-          2. wheels ordered via Wheel.support_index_min(self._supported_tags)
+          2. wheels ordered via Wheel.support_index_min(self._valid_tags)
           3. source archives
         If prefer_binary was set, then all wheels are sorted above sources.
 
@@ -537,7 +498,7 @@ class CandidateEvaluator(object):
               comparison operators, but then different sdist links
               with the same version, would have to be considered equal
         """
-        valid_tags = self._supported_tags
+        valid_tags = self._target_python.get_tags()
         support_num = len(valid_tags)
         build_tag = tuple()  # type: BuildTag
         binary_preference = 0
@@ -545,7 +506,7 @@ class CandidateEvaluator(object):
         if link.is_wheel:
             # can raise InvalidWheelFilename
             wheel = Wheel(link.filename)
-            if not wheel.supported(valid_tags):
+            if not self._is_wheel_supported(wheel):
                 raise UnsupportedWheel(
                     "%s is not a supported wheel for this platform. It "
                     "can't be sorted." % wheel.filename
@@ -604,20 +565,21 @@ class FoundCandidates(object):
 
     def __init__(
         self,
-        candidates,             # type: List[InstallationCandidate]
-        applicable_candidates,  # type: List[InstallationCandidate]
-        evaluator,              # type: CandidateEvaluator
+        candidates,     # type: List[InstallationCandidate]
+        versions,       # type: Set[str]
+        evaluator,      # type: CandidateEvaluator
     ):
         # type: (...) -> None
         """
         :param candidates: A sequence of all available candidates found.
-        :param applicable_candidates: The applicable candidates.
+        :param versions: The applicable versions to filter applicable
+            candidates.
         :param evaluator: A CandidateEvaluator object to sort applicable
             candidates by order of preference.
         """
-        self._applicable_candidates = applicable_candidates
         self._candidates = candidates
         self._evaluator = evaluator
+        self._versions = versions
 
     def iter_all(self):
         # type: () -> Iterable[InstallationCandidate]
@@ -627,9 +589,11 @@ class FoundCandidates(object):
 
     def iter_applicable(self):
         # type: () -> Iterable[InstallationCandidate]
-        """Iterate through the applicable candidates.
+        """Iterate through candidates matching the versions associated with
+        this instance.
         """
-        return iter(self._applicable_candidates)
+        # Again, converting version to str to deal with debundling.
+        return (c for c in self.iter_all() if str(c.version) in self._versions)
 
     def get_best(self):
         # type: () -> Optional[InstallationCandidate]
@@ -652,11 +616,8 @@ class PackageFinder(object):
         candidate_evaluator,  # type: CandidateEvaluator
         search_scope,         # type: SearchScope
         session,  # type: PipSession
-        target_python,        # type: TargetPython
-        allow_yanked,         # type: bool
         format_control=None,  # type: Optional[FormatControl]
         trusted_hosts=None,   # type: Optional[List[str]]
-        ignore_requires_python=None,  # type: Optional[bool]
     ):
         # type: (...) -> None
         """
@@ -674,10 +635,6 @@ class PackageFinder(object):
 
         format_control = format_control or FormatControl(set(), set())
 
-        self._allow_yanked = allow_yanked
-        self._ignore_requires_python = ignore_requires_python
-        self._target_python = target_python
-
         self.candidate_evaluator = candidate_evaluator
         self.search_scope = search_scope
         self.session = session
@@ -708,34 +665,28 @@ class PackageFinder(object):
         :param trusted_hosts: Domains not to emit warnings for when not using
             HTTPS.
         :param session: The Session to use to make requests.
-        :param target_python: The target Python interpreter to use when
-            checking compatibility. If None (the default), a TargetPython
-            object will be constructed from the running Python.
+        :param target_python: The target Python interpreter.
         """
         if session is None:
             raise TypeError(
                 "PackageFinder.create() missing 1 required keyword argument: "
                 "'session'"
             )
-        if target_python is None:
-            target_python = TargetPython()
 
-        supported_tags = target_python.get_tags()
         candidate_evaluator = CandidateEvaluator(
-            supported_tags=supported_tags,
+            allow_yanked=selection_prefs.allow_yanked,
+            target_python=target_python,
             prefer_binary=selection_prefs.prefer_binary,
             allow_all_prereleases=selection_prefs.allow_all_prereleases,
+            ignore_requires_python=selection_prefs.ignore_requires_python,
         )
 
         return cls(
             candidate_evaluator=candidate_evaluator,
             search_scope=search_scope,
             session=session,
-            target_python=target_python,
-            allow_yanked=selection_prefs.allow_yanked,
             format_control=selection_prefs.format_control,
             trusted_hosts=trusted_hosts,
-            ignore_requires_python=selection_prefs.ignore_requires_python,
         )
 
     @property
@@ -914,20 +865,6 @@ class PackageFinder(object):
 
         return False
 
-    def make_link_evaluator(self, project_name):
-        # type: (str) -> LinkEvaluator
-        canonical_name = canonicalize_name(project_name)
-        formats = self.format_control.get_allowed_formats(canonical_name)
-
-        return LinkEvaluator(
-            project_name=project_name,
-            canonical_name=canonical_name,
-            formats=formats,
-            target_python=self._target_python,
-            allow_yanked=self._allow_yanked,
-            ignore_requires_python=self._ignore_requires_python,
-        )
-
     def find_all_candidates(self, project_name):
         # type: (str) -> List[InstallationCandidate]
         """Find all available InstallationCandidate for project_name
@@ -935,7 +872,7 @@ class PackageFinder(object):
         This checks index_urls and find_links.
         All versions found are returned as an InstallationCandidate list.
 
-        See LinkEvaluator.evaluate_link() for details on which files
+        See CandidateEvaluator.evaluate_link() for details on which files
         are accepted.
         """
         search_scope = self.search_scope
@@ -966,11 +903,13 @@ class PackageFinder(object):
         for location in url_locations:
             logger.debug('* %s', location)
 
-        link_evaluator = self.make_link_evaluator(project_name)
+        canonical_name = canonicalize_name(project_name)
+        formats = self.format_control.get_allowed_formats(canonical_name)
+        search = Search(project_name, canonical_name, formats)
         find_links_versions = self._package_versions(
-            link_evaluator,
             # We trust every directly linked archive in find_links
             (Link(url, '-f') for url in self.find_links),
+            search
         )
 
         page_versions = []
@@ -978,10 +917,10 @@ class PackageFinder(object):
             logger.debug('Analyzing links from page %s', page.url)
             with indent_log():
                 page_versions.extend(
-                    self._package_versions(link_evaluator, page.iter_links())
+                    self._package_versions(page.iter_links(), search)
                 )
 
-        file_versions = self._package_versions(link_evaluator, file_locations)
+        file_versions = self._package_versions(file_locations, search)
         if file_versions:
             file_versions.sort(reverse=True)
             logger.debug(
@@ -1136,31 +1075,35 @@ class PackageFinder(object):
             logger.debug(u'Skipping link: %s: %s', reason, link)
             self._logged_links.add(link)
 
-    def get_install_candidate(self, link_evaluator, link):
-        # type: (LinkEvaluator, Link) -> Optional[InstallationCandidate]
+    def get_install_candidate(self, link, search):
+        # type: (Link, Search) -> Optional[InstallationCandidate]
         """
         If the link is a candidate for install, convert it to an
         InstallationCandidate and return it. Otherwise, return None.
         """
-        is_candidate, result = link_evaluator.evaluate_link(link)
+        is_candidate, result = (
+            self.candidate_evaluator.evaluate_link(link, search=search)
+        )
         if not is_candidate:
             if result:
                 self._log_skipped_link(link, reason=result)
             return None
 
         return InstallationCandidate(
-            project=link_evaluator.project_name,
-            location=link,
             # Convert the Text result to str since InstallationCandidate
             # accepts str.
-            version=str(result),
+            search.supplied, location=link, version=str(result),
         )
 
-    def _package_versions(self, link_evaluator, links):
-        # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate]
+    def _package_versions(
+        self,
+        links,  # type: Iterable[Link]
+        search  # type: Search
+    ):
+        # type: (...) -> List[InstallationCandidate]
         result = []
         for link in self._sort_links(links):
-            candidate = self.get_install_candidate(link_evaluator, link)
+            candidate = self.get_install_candidate(link, search=search)
             if candidate is not None:
                 result.append(candidate)
         return result
@@ -1329,3 +1272,13 @@ class HTMLPage(object):
             if link is None:
                 continue
             yield link
+
+
+Search = namedtuple('Search', 'supplied canonical formats')
+"""Capture key aspects of a search.
+
+:attribute supplied: The user supplied package.
+:attribute canonical: The canonical package name.
+:attribute formats: The formats allowed for this package. Should be a set
+    with 'binary' or 'source' or both in it.
+"""
diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py
index a23b79c4e..7ad5786c4 100644
--- a/src/pip/_internal/models/target_python.py
+++ b/src/pip/_internal/models/target_python.py
@@ -82,9 +82,7 @@ class TargetPython(object):
     def get_tags(self):
         # type: () -> List[Pep425Tag]
         """
-        Return the supported PEP 425 tags to check wheel candidates against.
-
-        The tags are returned in order of preference (most preferred first).
+        Return the supported tags to check wheel candidates against.
         """
         if self._valid_tags is None:
             # Pass versions=None if no py_version_info was given since
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 5192ab2fd..59305df02 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -595,7 +595,7 @@ class InstallRequirement(object):
                 'Running setup.py (path:%s) egg_info for package from %s',
                 self.setup_py_path, self.link,
             )
-        script = SETUPTOOLS_SHIM % self.setup_py_path
+        script = SETUPTOOLS_SHIM.format(self.setup_py_path)
         base_cmd = [sys.executable, '-c', script]
         if self.isolated:
             base_cmd += ["--no-user-cfg"]
@@ -757,7 +757,7 @@ class InstallRequirement(object):
                     [
                         sys.executable,
                         '-c',
-                        SETUPTOOLS_SHIM % self.setup_py_path
+                        SETUPTOOLS_SHIM.format(self.setup_py_path)
                     ] +
                     list(global_options) +
                     ['develop', '--no-deps'] +
@@ -1004,7 +1004,7 @@ class InstallRequirement(object):
         # type: (...) -> List[str]
         install_args = [sys.executable, "-u"]
         install_args.append('-c')
-        install_args.append(SETUPTOOLS_SHIM % self.setup_py_path)
+        install_args.append(SETUPTOOLS_SHIM.format(self.setup_py_path))
         install_args += list(global_options) + \
             ['install', '--record', record_filename]
         install_args += ['--single-version-externally-managed']
diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py
index ec3995c29..0250c07ec 100644
--- a/src/pip/_internal/utils/compat.py
+++ b/src/pip/_internal/utils/compat.py
@@ -15,7 +15,7 @@ from pip._vendor.urllib3.util import IS_PYOPENSSL
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
 if MYPY_CHECK_RUNNING:
-    from typing import Optional, Text, Tuple, Union
+    from typing import Tuple, Text
 
 try:
     import _ssl  # noqa
@@ -83,29 +83,18 @@ else:
     backslashreplace_decode = "backslashreplace_decode"
 
 
-def str_to_display(data, desc=None):
-    # type: (Union[bytes, Text], Optional[str]) -> Text
-    """
-    For display or logging purposes, convert a bytes object (or text) to
-    text (e.g. unicode in Python 2) safe for output.
-
-    :param desc: An optional phrase describing the input data, for use in
-        the log message if a warning is logged. Defaults to "Bytes object".
-
-    This function should never error out and so can take a best effort
-    approach. It is okay to be lossy if needed since the return value is
-    just for display.
+def console_to_str(data):
+    # type: (bytes) -> Text
+    """Return a string, safe for output, of subprocess output.
 
-    We assume the data is in the locale preferred encoding. If it won't
-    decode properly, we warn the user but decode as best we can.
+    We assume the data is in the locale preferred encoding.
+    If it won't decode properly, we warn the user but decode as
+    best we can.
 
-    We also ensure that the output can be safely written to standard output
-    without encoding errors.
+    We also ensure that the output can be safely written to
+    standard output without encoding errors.
     """
-    if isinstance(data, text_type):
-        return data
 
-    # Otherwise, data is a bytes object (str in Python 2).
     # First, get the encoding we assume. This is the preferred
     # encoding for the locale, unless that is not found, or
     # it is ASCII, in which case assume UTF-8
@@ -118,10 +107,10 @@ def str_to_display(data, desc=None):
     try:
         decoded_data = data.decode(encoding)
     except UnicodeDecodeError:
-        if desc is None:
-            desc = 'Bytes object'
-        msg_format = '{} does not appear to be encoded as %s'.format(desc)
-        logger.warning(msg_format, encoding)
+        logger.warning(
+            "Subprocess output does not appear to be encoded as %s",
+            encoding,
+        )
         decoded_data = data.decode(encoding, errors=backslashreplace_decode)
 
     # Make sure we can print the output, by encoding it to the output
@@ -149,13 +138,6 @@ def str_to_display(data, desc=None):
     return decoded_data
 
 
-def console_to_str(data):
-    # type: (bytes) -> Text
-    """Return a string, safe for output, of subprocess output.
-    """
-    return str_to_display(data, desc='Subprocess output')
-
-
 if sys.version_info >= (3,):
     def native_str(s, replace=False):
         # type: (str, bool) -> str
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index f42fa5f74..c9d689453 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -35,7 +35,7 @@ from pip._internal.locations import (
     write_delete_marker_file,
 )
 from pip._internal.utils.compat import (
-    WINDOWS, console_to_str, expanduser, stdlib_pkgs, str_to_display,
+    WINDOWS, console_to_str, expanduser, stdlib_pkgs,
 )
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
@@ -751,25 +751,19 @@ def make_subprocess_output_error(
     :param lines: A list of lines, each ending with a newline.
     """
     command = format_command_args(cmd_args)
-    # Convert `command` to text (unicode in Python 2) so we can use it as
-    # an argument in the unicode format string below. This avoids
-    # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2
-    # when the formatted command contains a non-ascii character.
-    command_display = str_to_display(command, desc='command bytes')
-
     # We know the joined output value ends in a newline.
     output = ''.join(lines)
     msg = (
-        # Use a unicode string to avoid "UnicodeEncodeError: 'ascii'
-        # codec can't encode character ..." in Python 2 when a format
-        # argument (e.g. `output`) has a non-ascii character.
+        # We need to mark this explicitly as a unicode string to avoid
+        # "UnicodeEncodeError: 'ascii' codec can't encode character ..."
+        # errors in Python 2 since e.g. `output` is a unicode string.
         u'Command errored out with exit status {exit_status}:\n'
-        ' command: {command_display}\n'
+        ' command: {command}\n'
         '     cwd: {cwd}\n'
         'Complete output ({line_count} lines):\n{output}{divider}'
     ).format(
         exit_status=exit_status,
-        command_display=command_display,
+        command=command,
         cwd=cwd,
         line_count=len(lines),
         output=output,
diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index 03973e976..207c6fb96 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -1,6 +1,11 @@
 # Shim to wrap setup.py invocation with setuptools
+#
+# We set sys.argv[0] to the path to the underlying setup.py file so
+# setuptools / distutils don't take the path to the setup.py to be "-c" when
+# invoking via the shim.  This avoids e.g. the following manifest_maker
+# warning: "warning: manifest_maker: standard file '-c' not found".
 SETUPTOOLS_SHIM = (
-    "import setuptools, tokenize;__file__=%r;"
+    "import sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};"
     "f=getattr(tokenize, 'open', open)(__file__);"
     "code=f.read().replace('\\r\\n', '\\n');"
     "f.close();"
diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py
index 46390f4a5..0902ce71e 100644
--- a/src/pip/_internal/utils/ui.py
+++ b/src/pip/_internal/utils/ui.py
@@ -208,8 +208,8 @@ class DownloadSilentBar(BaseDownloadProgressBar, SilentBar):  # type: ignore
     pass
 
 
-class DownloadBar(BaseDownloadProgressBar,  # type: ignore
-                  Bar):
+class DownloadIncrementalBar(BaseDownloadProgressBar,  # type: ignore
+                             IncrementalBar):
     pass
 
 
@@ -252,7 +252,7 @@ class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin,
 BAR_TYPES = {
     "off": (DownloadSilentBar, DownloadSilentBar),
     "on": (DefaultDownloadProgressBar, DownloadProgressSpinner),
-    "ascii": (DownloadBar, DownloadProgressSpinner),
+    "ascii": (DownloadIncrementalBar, DownloadProgressSpinner),
     "pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner),
     "emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner)
 }
diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py
index a7350c9ec..a3934acba 100644
--- a/src/pip/_internal/wheel.py
+++ b/src/pip/_internal/wheel.py
@@ -930,7 +930,7 @@ class WheelBuilder(object):
         # virtualenv.
         return [
             sys.executable, '-u', '-c',
-            SETUPTOOLS_SHIM % req.setup_py_path,
+            SETUPTOOLS_SHIM.format(req.setup_py_path)
         ] + list(self.global_options)
 
     def _build_one_pep517(self, req, tempd, python_tag=None):


Test patch:
diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py
index 6f5fe5b51..5f9cd375e 100644
--- a/tests/unit/test_compat.py
+++ b/tests/unit/test_compat.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import locale
 import os
 
@@ -7,7 +5,7 @@ import pytest
 
 import pip._internal.utils.compat as pip_compat
 from pip._internal.utils.compat import (
-    console_to_str, expanduser, get_path_uid, native_str, str_to_display,
+    console_to_str, expanduser, get_path_uid, native_str,
 )
 
 
@@ -47,58 +45,6 @@ def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch):
         get_path_uid(fs)
 
 
-@pytest.mark.parametrize('data, expected', [
-    ('abc', u'abc'),
-    # Test text (unicode in Python 2) input.
-    (u'abc', u'abc'),
-    # Test text input with non-ascii characters.
-    (u'déf', u'déf'),
-])
-def test_str_to_display(data, expected):
-    actual = str_to_display(data)
-    assert actual == expected, (
-        # Show the encoding for easier troubleshooting.
-        'encoding: {!r}'.format(locale.getpreferredencoding())
-    )
-
-
-@pytest.mark.parametrize('data, encoding, expected', [
-    # Test str input with non-ascii characters.
-    ('déf', 'utf-8', u'déf'),
-    # Test bytes input with non-ascii characters:
-    (u'déf'.encode('utf-8'), 'utf-8', u'déf'),
-    # Test a Windows encoding.
-    (u'déf'.encode('cp1252'), 'cp1252', u'déf'),
-    # Test a Windows encoding with incompatibly encoded text.
-    (u'déf'.encode('utf-8'), 'cp1252', u'dÃ©f'),
-])
-def test_str_to_display__encoding(monkeypatch, data, encoding, expected):
-    monkeypatch.setattr(locale, 'getpreferredencoding', lambda: encoding)
-    actual = str_to_display(data)
-    assert actual == expected, (
-        # Show the encoding for easier troubleshooting.
-        'encoding: {!r}'.format(locale.getpreferredencoding())
-    )
-
-
-def test_str_to_display__decode_error(monkeypatch, caplog):
-    monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8')
-    # Encode with an incompatible encoding.
-    data = u'ab'.encode('utf-16')
-    actual = str_to_display(data)
-
-    assert actual == u'\\xff\\xfea\x00b\x00', (
-        # Show the encoding for easier troubleshooting.
-        'encoding: {!r}'.format(locale.getpreferredencoding())
-    )
-    assert len(caplog.records) == 1
-    record = caplog.records[0]
-    assert record.levelname == 'WARNING'
-    assert record.message == (
-        'Bytes object does not appear to be encoded as utf-8'
-    )
-
-
 def test_console_to_str(monkeypatch):
     some_bytes = b"a\xE9\xC3\xE9b"
     encodings = ('ascii', 'utf-8', 'iso-8859-1', 'iso-8859-5',
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 59c7b5ac7..7a79fb51f 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -3,7 +3,7 @@ import sys
 
 import pytest
 from mock import Mock, patch
-from pkg_resources import parse_version
+from pkg_resources import Distribution, parse_version
 
 import pip._internal.pep425tags
 import pip._internal.wheel
@@ -11,7 +11,7 @@ from pip._internal.exceptions import (
     BestVersionAlreadyInstalled, DistributionNotFound,
 )
 from pip._internal.index import (
-    CandidateEvaluator, InstallationCandidate, Link, LinkEvaluator,
+    CandidateEvaluator, InstallationCandidate, Link, Search,
 )
 from pip._internal.models.target_python import TargetPython
 from pip._internal.req.constructors import install_req_from_line
@@ -216,7 +216,12 @@ class TestWheel:
             ('pyT', 'TEST', 'any'),
             ('pyT', 'none', 'any'),
         ]
-        evaluator = CandidateEvaluator(supported_tags=valid_tags)
+        target_python = TargetPython()
+        target_python._valid_tags = valid_tags
+        evaluator = CandidateEvaluator(
+            allow_yanked=True,
+            target_python=target_python,
+        )
         sort_key = evaluator._sort_key
         results = sorted(links, key=sort_key, reverse=True)
         results2 = sorted(reversed(links), key=sort_key, reverse=True)
@@ -414,17 +419,17 @@ def test_finder_installs_pre_releases_with_version_spec():
         assert link.url == "https://foo/bar-2.0b1.tar.gz"
 
 
-class TestLinkEvaluator(object):
+class TestCandidateEvaluator(object):
 
-    def make_test_link_evaluator(self, formats):
-        target_python = TargetPython()
-        return LinkEvaluator(
-            project_name='pytest',
-            canonical_name='pytest',
-            formats=formats,
-            target_python=target_python,
-            allow_yanked=True,
-        )
+    # patch this for travis which has distribute in its base env for now
+    @patch(
+        'pip._internal.wheel.pkg_resources.get_distribution',
+        lambda x: Distribution(project_name='setuptools', version='0.9')
+    )
+    def setup(self):
+        self.search_name = 'pytest'
+        self.canonical_name = 'pytest'
+        self.evaluator = CandidateEvaluator(allow_yanked=True)
 
     @pytest.mark.parametrize('url, expected_version', [
         ('http:/yo/pytest-1.0.tar.gz', '1.0'),
@@ -433,8 +438,12 @@ class TestLinkEvaluator(object):
     def test_evaluate_link__match(self, url, expected_version):
         """Test that 'pytest' archives match for 'pytest'"""
         link = Link(url)
-        evaluator = self.make_test_link_evaluator(formats=['source', 'binary'])
-        actual = evaluator.evaluate_link(link)
+        search = Search(
+            supplied=self.search_name,
+            canonical=self.canonical_name,
+            formats=['source', 'binary'],
+        )
+        actual = self.evaluator.evaluate_link(link, search)
         assert actual == (True, expected_version)
 
     @pytest.mark.parametrize('url, expected_msg', [
@@ -448,8 +457,12 @@ class TestLinkEvaluator(object):
     def test_evaluate_link__substring_fails(self, url, expected_msg):
         """Test that 'pytest<something> archives won't match for 'pytest'."""
         link = Link(url)
-        evaluator = self.make_test_link_evaluator(formats=['source', 'binary'])
-        actual = evaluator.evaluate_link(link)
+        search = Search(
+            supplied=self.search_name,
+            canonical=self.canonical_name,
+            formats=['source', 'binary'],
+        )
+        actual = self.evaluator.evaluate_link(link, search)
         assert actual == (False, expected_msg)
 
 
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 2d32867bd..b653660d2 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -4,14 +4,12 @@ import os.path
 import pytest
 from mock import Mock
 from pip._vendor import html5lib, requests
-from pip._vendor.packaging.specifiers import SpecifierSet
 
 from pip._internal.download import PipSession
 from pip._internal.index import (
-    CandidateEvaluator, FormatControl, HTMLPage, Link, LinkEvaluator,
-    PackageFinder, _check_link_requires_python, _clean_link,
-    _determine_base_url, _extract_version_from_fragment,
-    _find_name_version_sep, _get_html_page,
+    CandidateEvaluator, HTMLPage, Link, PackageFinder, Search,
+    _check_link_requires_python, _clean_link, _determine_base_url,
+    _extract_version_from_fragment, _find_name_version_sep, _get_html_page,
 )
 from pip._internal.models.candidate import InstallationCandidate
 from pip._internal.models.search_scope import SearchScope
@@ -86,7 +84,32 @@ def test_check_link_requires_python__invalid_requires(caplog):
     check_caplog(caplog, 'DEBUG', expected_message)
 
 
-class TestLinkEvaluator:
+class TestCandidateEvaluator:
+
+    def test_init__target_python(self):
+        """
+        Test the target_python argument.
+        """
+        target_python = TargetPython(py_version_info=(3, 7, 3))
+        evaluator = CandidateEvaluator(
+            allow_yanked=True,
+            target_python=target_python,
+        )
+        # The target_python attribute should be set as is.
+        assert evaluator._target_python is target_python
+
+    def test_init__target_python_none(self):
+        """
+        Test passing None for the target_python argument.
+        """
+        evaluator = CandidateEvaluator(
+            allow_yanked=True,
+            target_python=None,
+        )
+        # Spot-check the default TargetPython object.
+        actual_target_python = evaluator._target_python
+        assert actual_target_python._given_py_version_info is None
+        assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO
 
     @pytest.mark.parametrize(
         'py_version_info,ignore_requires_python,expected', [
@@ -101,19 +124,19 @@ class TestLinkEvaluator:
         self, py_version_info, ignore_requires_python, expected,
     ):
         target_python = TargetPython(py_version_info=py_version_info)
-        evaluator = LinkEvaluator(
-            project_name='twine',
-            canonical_name='twine',
-            formats={'source'},
-            target_python=target_python,
+        evaluator = CandidateEvaluator(
             allow_yanked=True,
+            target_python=target_python,
             ignore_requires_python=ignore_requires_python,
         )
         link = Link(
             'https://example.com/#egg=twine-1.12',
             requires_python='== 3.6.5',
         )
-        actual = evaluator.evaluate_link(link)
+        search = Search(
+            supplied='twine', canonical='twine', formats=['source'],
+        )
+        actual = evaluator.evaluate_link(link, search=search)
         assert actual == expected
 
     @pytest.mark.parametrize('yanked_reason, allow_yanked, expected', [
@@ -132,19 +155,15 @@ class TestLinkEvaluator:
     def test_evaluate_link__allow_yanked(
         self, yanked_reason, allow_yanked, expected,
     ):
-        target_python = TargetPython(py_version_info=(3, 6, 4))
-        evaluator = LinkEvaluator(
-            project_name='twine',
-            canonical_name='twine',
-            formats={'source'},
-            target_python=target_python,
-            allow_yanked=allow_yanked,
-        )
+        evaluator = CandidateEvaluator(allow_yanked=allow_yanked)
         link = Link(
             'https://example.com/#egg=twine-1.12',
             yanked_reason=yanked_reason,
         )
-        actual = evaluator.evaluate_link(link)
+        search = Search(
+            supplied='twine', canonical='twine', formats=['source'],
+        )
+        actual = evaluator.evaluate_link(link, search=search)
         assert actual == expected
 
     def test_evaluate_link__incompatible_wheel(self):
@@ -154,50 +173,20 @@ class TestLinkEvaluator:
         target_python = TargetPython(py_version_info=(3, 6, 4))
         # Set the valid tags to an empty list to make sure nothing matches.
         target_python._valid_tags = []
-        evaluator = LinkEvaluator(
-            project_name='sample',
-            canonical_name='sample',
-            formats={'binary'},
-            target_python=target_python,
+        evaluator = CandidateEvaluator(
             allow_yanked=True,
+            target_python=target_python,
         )
         link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl')
-        actual = evaluator.evaluate_link(link)
+        search = Search(
+            supplied='sample', canonical='sample', formats=['binary'],
+        )
+        actual = evaluator.evaluate_link(link, search=search)
         expected = (
             False, "none of the wheel's tags match: py2-none-any, py3-none-any"
         )
         assert actual == expected
 
-
-class TestCandidateEvaluator:
-
-    def make_mock_candidate(self, version, yanked_reason=None):
-        url = 'https://example.com/pkg-{}.tar.gz'.format(version)
-        link = Link(url, yanked_reason=yanked_reason)
-        candidate = InstallationCandidate('mypackage', version, link)
-
-        return candidate
-
-    def test_make_found_candidates(self):
-        specifier = SpecifierSet('<= 1.11')
-        versions = ['1.10', '1.11', '1.12']
-        candidates = [
-            self.make_mock_candidate(version) for version in versions
-        ]
-        evaluator = CandidateEvaluator()
-        found_candidates = evaluator.make_found_candidates(
-            candidates, specifier=specifier,
-        )
-
-        assert found_candidates._candidates == candidates
-        assert found_candidates._evaluator is evaluator
-        expected_applicable = candidates[:2]
-        assert [str(c.version) for c in expected_applicable] == [
-            '1.10',
-            '1.11',
-        ]
-        assert found_candidates._applicable_candidates == expected_applicable
-
     @pytest.mark.parametrize('yanked_reason, expected', [
         # Test a non-yanked file.
         (None, 0),
@@ -212,17 +201,24 @@ class TestCandidateEvaluator:
         link = Link(url, yanked_reason=yanked_reason)
         candidate = InstallationCandidate('mypackage', '1.0', link)
 
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator(allow_yanked=True)
         sort_value = evaluator._sort_key(candidate)
         # Yanked / non-yanked is reflected in the first element of the tuple.
         actual = sort_value[0]
         assert actual == expected
 
+    def make_mock_candidate(self, version, yanked_reason=None):
+        url = 'https://example.com/pkg-{}.tar.gz'.format(version)
+        link = Link(url, yanked_reason=yanked_reason)
+        candidate = InstallationCandidate('mypackage', version, link)
+
+        return candidate
+
     def test_get_best_candidate__no_candidates(self):
         """
         Test passing an empty list.
         """
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator(allow_yanked=True)
         actual = evaluator.get_best_candidate([])
         assert actual is None
 
@@ -237,7 +233,7 @@ class TestCandidateEvaluator:
             self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
         ]
         expected_best = candidates[1]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator(allow_yanked=True)
         actual = evaluator.get_best_candidate(candidates)
         assert actual is expected_best
         assert str(actual.version) == '3.0'
@@ -268,7 +264,7 @@ class TestCandidateEvaluator:
         candidates = [
             self.make_mock_candidate('1.0', yanked_reason=yanked_reason),
         ]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator(allow_yanked=True)
         actual = evaluator.get_best_candidate(candidates)
         assert str(actual.version) == '1.0'
 
@@ -295,7 +291,7 @@ class TestCandidateEvaluator:
             self.make_mock_candidate('1.0'),
         ]
         expected_best = candidates[1]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator(allow_yanked=True)
         actual = evaluator.get_best_candidate(candidates)
         assert actual is expected_best
         assert str(actual.version) == '2.0'
@@ -306,115 +302,42 @@ class TestCandidateEvaluator:
 
 class TestPackageFinder:
 
-    @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
-        (False, False),
-        (False, True),
-        (True, False),
-        (True, True),
-    ])
-    def test_create__candidate_evaluator(
-        self, allow_all_prereleases, prefer_binary,
-    ):
-        """
-        Test that the candidate_evaluator attribute is set correctly.
-        """
-        selection_prefs = SelectionPreferences(
-            allow_yanked=True,
-            allow_all_prereleases=allow_all_prereleases,
-            prefer_binary=prefer_binary,
-        )
-        target_python = TargetPython(py_version_info=(3, 7, 3))
-        target_python._valid_tags = ['tag1', 'tag2']
-        finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
-            selection_prefs=selection_prefs,
-            session=PipSession(),
-            target_python=target_python,
-        )
-        evaluator = finder.candidate_evaluator
-        assert evaluator.allow_all_prereleases == allow_all_prereleases
-        assert evaluator._prefer_binary == prefer_binary
-        assert evaluator._supported_tags == ['tag1', 'tag2']
-
-    def test_create__target_python(self):
-        """
-        Test that the _target_python attribute is set correctly.
-        """
-        target_python = TargetPython(py_version_info=(3, 7, 3))
-        finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
-            selection_prefs=SelectionPreferences(allow_yanked=True),
-            session=PipSession(),
-            target_python=target_python,
-        )
-        actual_target_python = finder._target_python
-        # The target_python attribute should be set as is.
-        assert actual_target_python is target_python
-        # Check that the attributes weren't reset.
-        assert actual_target_python.py_version_info == (3, 7, 3)
-
-    def test_create__target_python_none(self):
-        """
-        Test passing target_python=None.
-        """
-        finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
-            selection_prefs=SelectionPreferences(allow_yanked=True),
-            session=PipSession(),
-            target_python=None,
-        )
-        # Spot-check the default TargetPython object.
-        actual_target_python = finder._target_python
-        assert actual_target_python._given_py_version_info is None
-        assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO
-
     @pytest.mark.parametrize('allow_yanked', [False, True])
     def test_create__allow_yanked(self, allow_yanked):
         """
-        Test that the _allow_yanked attribute is set correctly.
-        """
-        selection_prefs = SelectionPreferences(allow_yanked=allow_yanked)
-        finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
-            selection_prefs=selection_prefs,
-            session=PipSession(),
-        )
-        assert finder._allow_yanked == allow_yanked
-
-    @pytest.mark.parametrize('ignore_requires_python', [False, True])
-    def test_create__ignore_requires_python(self, ignore_requires_python):
-        """
-        Test that the _ignore_requires_python attribute is set correctly.
+        Test that allow_yanked is passed to CandidateEvaluator.
         """
+        search_scope = SearchScope([], [])
         selection_prefs = SelectionPreferences(
-            allow_yanked=True,
-            ignore_requires_python=ignore_requires_python,
+            allow_yanked=allow_yanked,
         )
         finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
+            search_scope=search_scope,
             selection_prefs=selection_prefs,
-            session=PipSession(),
+            session=object(),
         )
-        assert finder._ignore_requires_python == ignore_requires_python
+        evaluator = finder.candidate_evaluator
+        assert evaluator._allow_yanked == allow_yanked
 
-    def test_create__format_control(self):
+    def test_create__target_python(self):
         """
-        Test that the format_control attribute is set correctly.
+        Test that target_python is passed to CandidateEvaluator as is.
         """
-        format_control = FormatControl(set(), {':all:'})
+        search_scope = SearchScope([], [])
         selection_prefs = SelectionPreferences(
             allow_yanked=True,
-            format_control=format_control,
         )
+        target_python = TargetPython(py_version_info=(3, 7, 3))
         finder = PackageFinder.create(
-            search_scope=SearchScope([], []),
+            search_scope=search_scope,
             selection_prefs=selection_prefs,
-            session=PipSession(),
+            session=object(),
+            target_python=target_python,
         )
-        actual_format_control = finder.format_control
-        assert actual_format_control is format_control
-        # Check that the attributes weren't reset.
-        assert actual_format_control.only_binary == {':all:'}
+        evaluator = finder.candidate_evaluator
+        actual_target_python = evaluator._target_python
+        assert actual_target_python is target_python
+        assert actual_target_python.py_version_info == (3, 7, 3)
 
     def test_add_trusted_host(self):
         # Leave a gap to test how the ordering is affected.
@@ -506,52 +429,6 @@ class TestPackageFinder:
         # Spot-check that SECURE_ORIGINS is included.
         assert actual[0] == ('https', '*', '*')
 
-    @pytest.mark.parametrize(
-        'allow_yanked, ignore_requires_python, only_binary, expected_formats',
-        [
-            (False, False, {}, frozenset({'binary', 'source'})),
-            # Test allow_yanked=True.
-            (True, False, {}, frozenset({'binary', 'source'})),
-            # Test ignore_requires_python=True.
-            (False, True, {}, frozenset({'binary', 'source'})),
-            # Test a non-trivial only_binary.
-            (False, False, {'twine'}, frozenset({'binary'})),
-        ]
-    )
-    def test_make_link_evaluator(
-        self, allow_yanked, ignore_requires_python, only_binary,
-        expected_formats,
-    ):
-        # Create a test TargetPython that we can check for.
-        target_python = TargetPython(py_version_info=(3, 7))
-        format_control = FormatControl(set(), only_binary)
-        finder = PackageFinder(
-            candidate_evaluator=CandidateEvaluator(),
-            search_scope=SearchScope([], []),
-            session=PipSession(),
-            target_python=target_python,
-            allow_yanked=allow_yanked,
-            format_control=format_control,
-            ignore_requires_python=ignore_requires_python,
-        )
-
-        # Pass a project_name that will be different from canonical_name.
-        link_evaluator = finder.make_link_evaluator('Twine')
-
-        assert link_evaluator.project_name == 'Twine'
-        assert link_evaluator._canonical_name == 'twine'
-        assert link_evaluator._allow_yanked == allow_yanked
-        assert link_evaluator._ignore_requires_python == ignore_requires_python
-        assert link_evaluator._formats == expected_formats
-
-        # Test the _target_python attribute.
-        actual_target_python = link_evaluator._target_python
-        # The target_python attribute should be set as is.
-        assert actual_target_python is target_python
-        # For good measure, check that the attributes weren't reset.
-        assert actual_target_python._given_py_version_info == (3, 7)
-        assert actual_target_python.py_version_info == (3, 7, 0)
-
 
 def test_sort_locations_file_expand_dir(data):
     """
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index b7be0d811..fdc1dd03c 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -6,7 +6,6 @@ util tests
 """
 import codecs
 import itertools
-import locale
 import os
 import shutil
 import stat
@@ -768,38 +767,10 @@ def test_make_subprocess_output_error():
     assert actual == expected, 'actual: {}'.format(actual)
 
 
-def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch):
-    """
-    Test a command argument with a non-ascii character.
-    """
-    cmd_args = ['foo', 'déf']
-    if sys.version_info[0] == 2:
-        # Check in Python 2 that the str (bytes object) with the non-ascii
-        # character has the encoding we expect. (This comes from the source
-        # code encoding at the top of the file.)
-        assert cmd_args[1].decode('utf-8') == u'déf'
-
-    # We need to monkeypatch so the encoding will be correct on Windows.
-    monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8')
-    actual = make_subprocess_output_error(
-        cmd_args=cmd_args,
-        cwd='/path/to/cwd',
-        lines=[],
-        exit_status=1,
-    )
-    expected = dedent(u"""\
-    Command errored out with exit status 1:
-     command: foo 'déf'
-         cwd: /path/to/cwd
-    Complete output (0 lines):
-    ----------------------------------------""")
-    assert actual == expected, u'actual: {}'.format(actual)
-
-
 # This test is mainly important for checking unicode in Python 2.
-def test_make_subprocess_output_error__non_ascii_line():
+def test_make_subprocess_output_error__unicode():
     """
-    Test a line with a non-ascii character.
+    Test a line with non-ascii unicode characters.
     """
     lines = [u'curly-quote: \u2018\n']
     actual = make_subprocess_output_error(
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 66754c667..03d2229d8 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -98,17 +98,14 @@ def test_rev_options_make_new():
     assert new_options.vc_class is Git
 
 
-@pytest.mark.parametrize('sha, expected', [
-    ((40 * 'a'), True),
-    ((40 * 'A'), True),
+def test_looks_like_hash():
+    assert looks_like_hash(40 * 'a')
+    assert looks_like_hash(40 * 'A')
     # Test a string containing all valid characters.
-    ((18 * 'a' + '0123456789abcdefABCDEF'), True),
-    ((40 * 'g'), False),
-    ((39 * 'a'), False),
-    ((41 * 'a'), False)
-])
-def test_looks_like_hash(sha, expected):
-    assert looks_like_hash(sha) == expected
+    assert looks_like_hash(18 * 'a' + '0123456789abcdefABCDEF')
+    assert not looks_like_hash(40 * 'g')
+    assert not looks_like_hash(39 * 'a')
+    assert not looks_like_hash(41 * 'a')
 
 
 @pytest.mark.parametrize('vcs_cls, remote_url, expected', [