Repository: pypa/pip

Issue:
Add tested function for setuptools shim args
There are four places in the code base that construct an argument list using `SETUPTOOLS_SHIM` that looks roughly like this (found while reviewing PR #5936)--

```python
[sys.executable, '-u', '-c', SETUPTOOLS_SHIM.format(setup_py_path)]
```

I think it would be good to add a unit-tested function to `setuptools_build.py` called something like `make_setuptools_shim_args(setup_py_path, unbuffered=False)` that contains this logic.


Discussion:

Patch:
diff --git a/news/6648.bugfix b/news/6648.bugfix
new file mode 100644
index 000000000..9f986fe17
--- /dev/null
+++ b/news/6648.bugfix
@@ -0,0 +1,1 @@
+Improve error message printed when an invalid editable requirement is provided.
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index 8a373ef08..801b4ea31 100644
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -189,7 +189,7 @@ class ListCommand(Command):
                     all_candidates = [candidate for candidate in all_candidates
                                       if not candidate.version.is_prerelease]
 
-                evaluator = finder.candidate_evaluator
+                evaluator = finder.make_candidate_evaluator()
                 best_candidate = evaluator.get_best_candidate(all_candidates)
                 if best_candidate is None:
                     continue
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index e0e78678c..1352de1a9 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -440,6 +440,26 @@ class LinkEvaluator(object):
         return (True, version)
 
 
+class CandidatePreferences(object):
+
+    """
+    Encapsulates some of the preferences for filtering and sorting
+    InstallationCandidate objects.
+    """
+
+    def __init__(
+        self,
+        prefer_binary=False,  # type: bool
+        allow_all_prereleases=False,  # type: bool
+    ):
+        # type: (...) -> None
+        """
+        :param allow_all_prereleases: Whether to allow all pre-releases.
+        """
+        self.allow_all_prereleases = allow_all_prereleases
+        self.prefer_binary = prefer_binary
+
+
 class CandidateEvaluator(object):
 
     """
@@ -447,46 +467,57 @@ class CandidateEvaluator(object):
     on what tags are valid.
     """
 
+    @classmethod
+    def create(
+        cls,
+        target_python=None,   # type: Optional[TargetPython]
+        prefer_binary=False,  # type: bool
+        allow_all_prereleases=False,  # type: bool
+    ):
+        # type: (...) -> CandidateEvaluator
+        """Create a CandidateEvaluator object.
+
+        :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.
+        """
+        if target_python is None:
+            target_python = TargetPython()
+
+        supported_tags = target_python.get_tags()
+
+        return cls(
+            supported_tags=supported_tags,
+            prefer_binary=prefer_binary,
+            allow_all_prereleases=allow_all_prereleases,
+        )
+
     def __init__(
         self,
-        supported_tags=None,  # type: Optional[List[Pep425Tag]]
+        supported_tags,       # type: 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.
+            Python in order of preference (most preferred first).
         """
-        if supported_tags is None:
-            target_python = TargetPython()
-            supported_tags = target_python.get_tags()
-
+        self._allow_all_prereleases = allow_all_prereleases
         self._prefer_binary = prefer_binary
         self._supported_tags = supported_tags
 
-        self.allow_all_prereleases = allow_all_prereleases
-
-    def make_found_candidates(
+    def get_applicable_candidates(
         self,
-        candidates,      # type: List[InstallationCandidate]
-        specifier=None,  # type: Optional[specifiers.BaseSpecifier]
+        candidates,  # type: List[InstallationCandidate]
+        specifier,   # type: specifiers.BaseSpecifier
     ):
-        # type: (...) -> FoundCandidates
+        # type: (...) -> List[InstallationCandidate]
         """
-        Create and return a `FoundCandidates` instance.
-
-        :param specifier: An optional object implementing `filter`
-            (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
-            versions.
+        Return the applicable candidates from a list of candidates.
         """
-        if specifier is None:
-            specifier = specifiers.SpecifierSet()
-
         # Using None infers from the specifier instead.
-        allow_prereleases = self.allow_all_prereleases or None
+        allow_prereleases = self._allow_all_prereleases or None
         versions = {
             str(v) for v in specifier.filter(
                 # We turn the version object into a str here because otherwise
@@ -505,6 +536,28 @@ class CandidateEvaluator(object):
         applicable_candidates = [
             c for c in candidates if str(c.version) in versions
         ]
+        return applicable_candidates
+
+    def make_found_candidates(
+        self,
+        candidates,      # type: List[InstallationCandidate]
+        specifier=None,  # type: Optional[specifiers.BaseSpecifier]
+    ):
+        # type: (...) -> FoundCandidates
+        """
+        Create and return a `FoundCandidates` instance.
+
+        :param specifier: An optional object implementing `filter`
+            (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
+            versions.
+        """
+        if specifier is None:
+            specifier = specifiers.SpecifierSet()
+
+        applicable_candidates = self.get_applicable_candidates(
+            candidates=candidates,
+            specifier=specifier,
+        )
 
         return FoundCandidates(
             candidates,
@@ -649,13 +702,13 @@ class PackageFinder(object):
 
     def __init__(
         self,
-        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]]
+        candidate_prefs=None,         # type: CandidatePreferences
         ignore_requires_python=None,  # type: Optional[bool]
     ):
         # type: (...) -> None
@@ -663,22 +716,25 @@ class PackageFinder(object):
         This constructor is primarily meant to be used by the create() class
         method and from tests.
 
-        :param candidate_evaluator: A CandidateEvaluator object.
         :param session: The Session to use to make requests.
         :param format_control: A FormatControl object, used to control
             the selection of source packages / binary packages when consulting
             the index and links.
+        :param candidate_prefs: Options to use when creating a
+            CandidateEvaluator object.
         """
         if trusted_hosts is None:
             trusted_hosts = []
+        if candidate_prefs is None:
+            candidate_prefs = CandidatePreferences()
 
         format_control = format_control or FormatControl(set(), set())
 
         self._allow_yanked = allow_yanked
+        self._candidate_prefs = candidate_prefs
         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
         self.format_control = format_control
@@ -720,15 +776,13 @@ class PackageFinder(object):
         if target_python is None:
             target_python = TargetPython()
 
-        supported_tags = target_python.get_tags()
-        candidate_evaluator = CandidateEvaluator(
-            supported_tags=supported_tags,
+        candidate_prefs = CandidatePreferences(
             prefer_binary=selection_prefs.prefer_binary,
             allow_all_prereleases=selection_prefs.allow_all_prereleases,
         )
 
         return cls(
-            candidate_evaluator=candidate_evaluator,
+            candidate_prefs=candidate_prefs,
             search_scope=search_scope,
             session=session,
             target_python=target_python,
@@ -751,11 +805,11 @@ class PackageFinder(object):
     @property
     def allow_all_prereleases(self):
         # type: () -> bool
-        return self.candidate_evaluator.allow_all_prereleases
+        return self._candidate_prefs.allow_all_prereleases
 
     def set_allow_all_prereleases(self):
         # type: () -> None
-        self.candidate_evaluator.allow_all_prereleases = True
+        self._candidate_prefs.allow_all_prereleases = True
 
     def add_trusted_host(self, host, source=None):
         # type: (str, Optional[str]) -> None
@@ -995,6 +1049,17 @@ class PackageFinder(object):
         # This is an intentional priority ordering
         return file_versions + find_links_versions + page_versions
 
+    def make_candidate_evaluator(self):
+        # type: (...) -> CandidateEvaluator
+        """Create a CandidateEvaluator object to use.
+        """
+        candidate_prefs = self._candidate_prefs
+        return CandidateEvaluator.create(
+            target_python=self._target_python,
+            prefer_binary=candidate_prefs.prefer_binary,
+            allow_all_prereleases=candidate_prefs.allow_all_prereleases,
+        )
+
     def find_candidates(
         self,
         project_name,       # type: str
@@ -1010,7 +1075,8 @@ class PackageFinder(object):
         :return: A `FoundCandidates` instance.
         """
         candidates = self.find_all_candidates(project_name)
-        return self.candidate_evaluator.make_found_candidates(
+        candidate_evaluator = self.make_candidate_evaluator()
+        return candidate_evaluator.make_found_candidates(
             candidates, specifier=specifier,
         )
 
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index d0b0c7153..cd0ab5042 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -109,9 +109,9 @@ def parse_editable(editable_req):
 
     if '+' not in url:
         raise InstallationError(
-            '%s should either be a path to a local project or a VCS url '
-            'beginning with svn+, git+, hg+, or bzr+' %
-            editable_req
+            '{} is not a valid editable requirement. '
+            'It should either be a path to a local project or a VCS URL '
+            '(beginning with svn+, git+, hg+, or bzr+).'.format(editable_req)
         )
 
     vc_type = url.split('+', 1)[0].lower()
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 59305df02..d92ed7513 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -33,7 +33,7 @@ from pip._internal.utils.misc import (
     get_installed_version, redact_password_from_url, rmtree,
 )
 from pip._internal.utils.packaging import get_metadata
-from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
+from pip._internal.utils.setuptools_build import make_setuptools_shim_args
 from pip._internal.utils.temp_dir import TempDirectory
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from pip._internal.utils.ui import open_spinner
@@ -595,8 +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.format(self.setup_py_path)
-        base_cmd = [sys.executable, '-c', script]
+        base_cmd = make_setuptools_shim_args(self.setup_py_path)
         if self.isolated:
             base_cmd += ["--no-user-cfg"]
         egg_info_cmd = base_cmd + ['egg_info']
@@ -754,11 +753,7 @@ class InstallRequirement(object):
             # FIXME: should we do --install-headers here too?
             with self.build_env:
                 call_subprocess(
-                    [
-                        sys.executable,
-                        '-c',
-                        SETUPTOOLS_SHIM.format(self.setup_py_path)
-                    ] +
+                    make_setuptools_shim_args(self.setup_py_path) +
                     list(global_options) +
                     ['develop', '--no-deps'] +
                     list(install_options),
@@ -1002,9 +997,8 @@ class InstallRequirement(object):
         pycompile  # type: bool
     ):
         # type: (...) -> List[str]
-        install_args = [sys.executable, "-u"]
-        install_args.append('-c')
-        install_args.append(SETUPTOOLS_SHIM.format(self.setup_py_path))
+        install_args = make_setuptools_shim_args(self.setup_py_path,
+                                                 unbuffered_output=True)
         install_args += list(global_options) + \
             ['install', '--record', record_filename]
         install_args += ['--single-version-externally-managed']
diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index 207c6fb96..589560726 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -1,13 +1,36 @@
+import sys
+
+from pip._internal.utils.typing import MYPY_CHECK_RUNNING
+
+if MYPY_CHECK_RUNNING:
+    from typing import List
+
 # 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 = (
+_SETUPTOOLS_SHIM = (
     "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();"
     "exec(compile(code, __file__, 'exec'))"
 )
+
+
+def make_setuptools_shim_args(setup_py_path, unbuffered_output=False):
+    # type: (str, bool) -> List[str]
+    """
+    Get setuptools command arguments with shim wrapped setup file invocation.
+
+    :param setup_py_path: The path to setup.py to be wrapped.
+    :param unbuffered_output: If True, adds the unbuffered switch to the
+     argument list.
+    """
+    args = [sys.executable]
+    if unbuffered_output:
+        args.append('-u')
+    args.extend(['-c', _SETUPTOOLS_SHIM.format(setup_py_path)])
+    return args
diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py
index a3934acba..7df127398 100644
--- a/src/pip/_internal/wheel.py
+++ b/src/pip/_internal/wheel.py
@@ -36,7 +36,7 @@ from pip._internal.utils.misc import (
     LOG_DIVIDER, call_subprocess, captured_stdout, ensure_dir,
     format_command_args, path_to_url, read_chunks,
 )
-from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM
+from pip._internal.utils.setuptools_build import make_setuptools_shim_args
 from pip._internal.utils.temp_dir import TempDirectory
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from pip._internal.utils.ui import open_spinner
@@ -928,10 +928,9 @@ class WheelBuilder(object):
         # isolating. Currently, it breaks Python in virtualenvs, because it
         # relies on site.py to find parts of the standard library outside the
         # virtualenv.
-        return [
-            sys.executable, '-u', '-c',
-            SETUPTOOLS_SHIM.format(req.setup_py_path)
-        ] + list(self.global_options)
+        base_cmd = make_setuptools_shim_args(req.setup_py_path,
+                                             unbuffered_output=True)
+        return base_cmd + list(self.global_options)
 
     def _build_one_pep517(self, req, tempd, python_tag=None):
         """Build one InstallRequirement using the PEP 517 build process.


Test patch:
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index b4c78af4b..0fa89c66c 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -230,7 +230,7 @@ def test_basic_editable_install(script):
     """
     result = script.pip('install', '-e', 'INITools==0.2', expect_error=True)
     assert (
-        "INITools==0.2 should either be a path to a local project or a VCS url"
+        "INITools==0.2 is not a valid editable requirement"
         in result.stderr
     )
     assert not result.files_created
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 59c7b5ac7..54fb1897b 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -242,8 +242,8 @@ class TestWheel:
                 Link("simplewheel-1.0-py2.py3-none-any.whl"),
             ),
         ]
-        finder = make_test_finder()
-        sort_key = finder.candidate_evaluator._sort_key
+        candidate_evaluator = CandidateEvaluator.create()
+        sort_key = candidate_evaluator._sort_key
         results = sorted(links, key=sort_key, reverse=True)
         results2 = sorted(reversed(links), key=sort_key, reverse=True)
         assert links == results == results2, results2
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 2d32867bd..d6ebf939d 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -8,8 +8,8 @@ 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,
+    CandidateEvaluator, CandidatePreferences, 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,
 )
@@ -17,6 +17,7 @@ from pip._internal.models.candidate import InstallationCandidate
 from pip._internal.models.search_scope import SearchScope
 from pip._internal.models.selection_prefs import SelectionPreferences
 from pip._internal.models.target_python import TargetPython
+from pip._internal.pep425tags import get_supported
 from tests.lib import CURRENT_PY_VERSION_INFO, make_test_finder
 
 
@@ -171,6 +172,32 @@ class TestLinkEvaluator:
 
 class TestCandidateEvaluator:
 
+    @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
+        (False, False),
+        (False, True),
+        (True, False),
+        (True, True),
+    ])
+    def test_create(self, allow_all_prereleases, prefer_binary):
+        target_python = TargetPython()
+        target_python._valid_tags = [('py36', 'none', 'any')]
+        evaluator = CandidateEvaluator.create(
+            target_python=target_python,
+            allow_all_prereleases=allow_all_prereleases,
+            prefer_binary=prefer_binary,
+        )
+        assert evaluator._allow_all_prereleases == allow_all_prereleases
+        assert evaluator._prefer_binary == prefer_binary
+        assert evaluator._supported_tags == [('py36', 'none', 'any')]
+
+    def test_create__target_python_none(self):
+        """
+        Test passing target_python=None.
+        """
+        evaluator = CandidateEvaluator.create()
+        expected_tags = get_supported()
+        assert evaluator._supported_tags == expected_tags
+
     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)
@@ -178,13 +205,30 @@ class TestCandidateEvaluator:
 
         return candidate
 
+    def test_get_applicable_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.create()
+        actual = evaluator.get_applicable_candidates(
+            candidates, specifier=specifier,
+        )
+        expected_applicable = candidates[:2]
+        assert [str(c.version) for c in expected_applicable] == [
+            '1.10',
+            '1.11',
+        ]
+        assert actual == expected_applicable
+
     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()
+        evaluator = CandidateEvaluator.create()
         found_candidates = evaluator.make_found_candidates(
             candidates, specifier=specifier,
         )
@@ -212,7 +256,7 @@ class TestCandidateEvaluator:
         link = Link(url, yanked_reason=yanked_reason)
         candidate = InstallationCandidate('mypackage', '1.0', link)
 
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator.create()
         sort_value = evaluator._sort_key(candidate)
         # Yanked / non-yanked is reflected in the first element of the tuple.
         actual = sort_value[0]
@@ -222,7 +266,7 @@ class TestCandidateEvaluator:
         """
         Test passing an empty list.
         """
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator.create()
         actual = evaluator.get_best_candidate([])
         assert actual is None
 
@@ -237,7 +281,7 @@ class TestCandidateEvaluator:
             self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
         ]
         expected_best = candidates[1]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator.create()
         actual = evaluator.get_best_candidate(candidates)
         assert actual is expected_best
         assert str(actual.version) == '3.0'
@@ -268,7 +312,7 @@ class TestCandidateEvaluator:
         candidates = [
             self.make_mock_candidate('1.0', yanked_reason=yanked_reason),
         ]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator.create()
         actual = evaluator.get_best_candidate(candidates)
         assert str(actual.version) == '1.0'
 
@@ -295,7 +339,7 @@ class TestCandidateEvaluator:
             self.make_mock_candidate('1.0'),
         ]
         expected_best = candidates[1]
-        evaluator = CandidateEvaluator()
+        evaluator = CandidateEvaluator.create()
         actual = evaluator.get_best_candidate(candidates)
         assert actual is expected_best
         assert str(actual.version) == '2.0'
@@ -312,29 +356,25 @@ class TestPackageFinder:
         (True, False),
         (True, True),
     ])
-    def test_create__candidate_evaluator(
+    def test_create__candidate_prefs(
         self, allow_all_prereleases, prefer_binary,
     ):
         """
-        Test that the candidate_evaluator attribute is set correctly.
+        Test that the _candidate_prefs 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']
+        candidate_prefs = finder._candidate_prefs
+        assert candidate_prefs.allow_all_prereleases == allow_all_prereleases
+        assert candidate_prefs.prefer_binary == prefer_binary
 
     def test_create__target_python(self):
         """
@@ -526,7 +566,6 @@ class TestPackageFinder:
         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,
@@ -552,6 +591,34 @@ class TestPackageFinder:
         assert actual_target_python._given_py_version_info == (3, 7)
         assert actual_target_python.py_version_info == (3, 7, 0)
 
+    @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
+        (False, False),
+        (False, True),
+        (True, False),
+        (True, True),
+    ])
+    def test_make_candidate_evaluator(
+        self, allow_all_prereleases, prefer_binary,
+    ):
+        target_python = TargetPython()
+        target_python._valid_tags = [('py36', 'none', 'any')]
+        candidate_prefs = CandidatePreferences(
+            prefer_binary=prefer_binary,
+            allow_all_prereleases=allow_all_prereleases,
+        )
+        finder = PackageFinder(
+            search_scope=SearchScope([], []),
+            session=PipSession(),
+            target_python=target_python,
+            allow_yanked=True,
+            candidate_prefs=candidate_prefs,
+        )
+
+        evaluator = finder.make_candidate_evaluator()
+        assert evaluator._allow_all_prereleases == allow_all_prereleases
+        assert evaluator._prefer_binary == prefer_binary
+        assert evaluator._supported_tags == [('py36', 'none', 'any')]
+
 
 def test_sort_locations_file_expand_dir(data):
     """
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 903b0ec59..cdf6c1e80 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -36,6 +36,7 @@ from pip._internal.utils.misc import (
     redact_netloc, redact_password_from_url, remove_auth_from_url, rmtree,
     split_auth_from_netloc, split_auth_netloc_from_url, untar_file, unzip_file,
 )
+from pip._internal.utils.setuptools_build import make_setuptools_shim_args
 from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
 from pip._internal.utils.ui import SpinnerInterface
 
@@ -1333,3 +1334,18 @@ def test_deprecated_message_reads_well():
         "You can find discussion regarding this at "
         "https://github.com/pypa/pip/issues/100000."
     )
+
+
+@pytest.mark.parametrize("unbuffered_output", [False, True])
+def test_make_setuptools_shim_args(unbuffered_output):
+    args = make_setuptools_shim_args(
+        "/dir/path/setup.py",
+        unbuffered_output=unbuffered_output
+    )
+
+    assert ("-u" in args) == unbuffered_output
+
+    assert args[-2] == "-c"
+
+    assert "sys.argv[0] = '/dir/path/setup.py'" in args[-1]
+    assert "__file__='/dir/path/setup.py'" in args[-1]