Repository: pex-tool/pex

Issue:
TypeError when resolving local platforms.
As seen in https://github.com/pantsbuild/example-python CI:
```
raceback (most recent call last):
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 423, in execute
    exit_code = self._wrap_coverage(self._wrap_profiling, self._execute)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 356, in _wrap_coverage
    return runner(*args)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 387, in _wrap_profiling
    return runner(*args)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 474, in _execute
    return self.execute_entry(self._pex_info.entry_point)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 576, in execute_entry
    return runner(entry_point)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.bootstrap/pex/pex.py", line 592, in execute_pkg_resources
    return runner()
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.deps/pex-2.1.16-py2.py3-none-any.whl/pex/bin/pex.py", line 970, in main
    pex_builder = build_pex(reqs, options, cache=ENV.PEX_ROOT)
  File "/home/travis/.pex/unzipped_pexes/49b4b32295a60a0a4dfc0659c3a44f93560c4a81/.deps/pex-2.1.16-py2.py3-none-any.whl/pex/bin/pex.py", line 762, in build_pex
    resolved_platforms = candidate_interpreter.supported_platforms.intersection(
AttributeError: 'tuple' object has no attribute 'supported_platforms'
```


Discussion:
This bug was introduced here: https://github.com/pantsbuild/pex/pull/1028#discussion_r495193476

Resolving patch:
diff --git a/pex/bin/pex.py b/pex/bin/pex.py
--- a/pex/bin/pex.py
+++ b/pex/bin/pex.py
@@ -750,7 +750,11 @@ def to_python_interpreter(full_path_or_basename):
             constraints = options.interpreter_constraint
             validate_constraints(constraints)
             try:
-                interpreters = list(iter_compatible_interpreters(pex_python_path, constraints))
+                interpreters = list(
+                    iter_compatible_interpreters(
+                        path=pex_python_path, interpreter_constraints=constraints
+                    )
+                )
             except UnsatisfiableInterpreterConstraintsError as e:
                 die(
                     e.create_message("Could not find a compatible interpreter."),
@@ -763,7 +767,7 @@ def to_python_interpreter(full_path_or_basename):
         with TRACER.timed(
             "Searching for local interpreters matching {}".format(", ".join(map(str, platforms)))
         ):
-            candidate_interpreters = OrderedSet(iter_compatible_interpreters(pex_python_path))
+            candidate_interpreters = OrderedSet(iter_compatible_interpreters(path=pex_python_path))
             candidate_interpreters.add(PythonInterpreter.get())
             for candidate_interpreter in candidate_interpreters:
                 resolved_platforms = candidate_interpreter.supported_platforms.intersection(
diff --git a/pex/interpreter_constraints.py b/pex/interpreter_constraints.py
--- a/pex/interpreter_constraints.py
+++ b/pex/interpreter_constraints.py
@@ -3,7 +3,7 @@
 
 # A library of functions for filtering Python interpreters based on compatibility constraints
 
-from __future__ import absolute_import, print_function
+from __future__ import absolute_import
 
 import os
 
@@ -14,6 +14,8 @@
 if TYPE_CHECKING:
     from typing import Iterable, Optional, Tuple
 
+    InterpreterIdentificationError = Tuple[str, str]
+
 
 def validate_constraints(constraints):
     # type: (Iterable[str]) -> None
@@ -30,11 +32,17 @@ def validate_constraints(constraints):
 class UnsatisfiableInterpreterConstraintsError(Exception):
     """Indicates interpreter constraints could not be satisfied."""
 
-    def __init__(self, constraints, candidates, failures):
-        # type: (Iterable[str], Iterable[PythonInterpreter], Iterable[Tuple[str, str]]) -> None
+    def __init__(
+        self,
+        constraints,  # type: Iterable[str]
+        candidates,  # type: Iterable[PythonInterpreter]
+        failures,  # type: Iterable[InterpreterIdentificationError]
+    ):
+        # type: (...) -> None
         """
         :param constraints: The constraints that could not be satisfied.
         :param candidates: The python interpreters that were compared against the constraints.
+        :param failures: Descriptions of the python interpreters that were unidentifiable.
         """
         self.constraints = tuple(constraints)
         self.candidates = tuple(candidates)
@@ -87,27 +95,38 @@ def create_message(self, preamble=None):
         interpreters_format = "{{index}}.) {{binary: >{}}} {{requirement}}".format(
             binary_column_width
         )
+
+        qualifier = ""
         if failures_message:
-            failures_message = (
-                "\n" "\n" "Skipped the following broken interpreters:\n" "{}"
-            ).format(failures_message)
+            failures_message = "Skipped the following broken interpreters:\n{}".format(
+                failures_message
+            )
+            qualifier = "working "
+
+        constraints_message = ""
+        if self.constraints:
+            constraints_message = (
+                "No {qualifier}interpreter compatible with the requested constraints was found:\n"
+                "  {constraints}"
+            ).format(qualifier=qualifier, constraints="\n  ".join(self.constraints))
+
+        problems = "\n\n".join(msg for msg in (failures_message, constraints_message) if msg)
+        if problems:
+            problems = "\n\n{}".format(problems)
+
         return (
             "{preamble}"
             "Examined the following {qualifier}interpreters:\n"
             "{interpreters}"
-            "{failures_message}\n"
-            "\n"
-            "No {qualifier}interpreter compatible with the requested constraints was found:\n"
-            "  {constraints}"
+            "{problems}"
         ).format(
             preamble=preamble,
-            qualifier="working " if failures_message else "",
+            qualifier=qualifier,
             interpreters="\n".join(
                 interpreters_format.format(
                     index=i, binary=candidate.binary, requirement=candidate.identity.requirement
                 )
                 for i, candidate in enumerate(self.candidates, start=1)
             ),
-            constraints="\n  ".join(self.constraints),
-            failures_message=failures_message,
+            problems=problems,
         )
diff --git a/pex/pex_bootstrapper.py b/pex/pex_bootstrapper.py
--- a/pex/pex_bootstrapper.py
+++ b/pex/pex_bootstrapper.py
@@ -1,162 +1,170 @@
 # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-from __future__ import absolute_import, print_function
+from __future__ import absolute_import
 
 import os
 import sys
 
 from pex import pex_warnings
 from pex.common import die
-from pex.executor import Executor
 from pex.interpreter import PythonInterpreter
 from pex.interpreter_constraints import UnsatisfiableInterpreterConstraintsError
 from pex.orderedset import OrderedSet
+from pex.pex_info import PexInfo
 from pex.tracer import TRACER
+from pex.typing import TYPE_CHECKING, cast
 from pex.variables import ENV
 
-__all__ = ("bootstrap_pex",)
-
+if TYPE_CHECKING:
+    from typing import (
+        Iterable,
+        Iterator,
+        List,
+        MutableSet,
+        NoReturn,
+        Optional,
+        Tuple,
+        Union,
+        Callable,
+    )
 
-def _iter_pex_python(pex_python):
-    def try_create(try_path):
-        try:
-            return PythonInterpreter.from_binary(try_path)
-        except Executor.ExecutionError:
-            return None
+    InterpreterIdentificationError = Tuple[str, str]
+    InterpreterOrError = Union[PythonInterpreter, InterpreterIdentificationError]
+    PathFilter = Callable[[str], bool]
+
+
+# TODO(John Sirois): Move this to interpreter_constraints.py. As things stand, both pex/bin/pex.py
+#  and this file use this function. The Pex CLI should not depend on this file which hosts code
+#  used at PEX runtime.
+def iter_compatible_interpreters(
+    path=None,  # type: Optional[str]
+    valid_basenames=None,  # type: Optional[Iterable[str]]
+    interpreter_constraints=None,  # type: Optional[Iterable[str]]
+):
+    # type: (...) -> Iterator[PythonInterpreter]
+    """Find all compatible interpreters on the system within the supplied constraints.
+
+    :param path: A PATH-style string with files or directories separated by os.pathsep.
+    :param valid_basenames: Valid basenames for discovered interpreter binaries. If not specified,
+                            Then all typical names are accepted (i.e.: python, python3, python2.7,
+                            pypy, etc.).
+    :param interpreter_constraints: Interpreter type and version constraint strings as described in
+                                    `--interpreter-constraint`.
+
+    Interpreters are searched for in `path` if specified and $PATH if not.
+
+    If no interpreters are found and there are no further constraints (neither `valid_basenames` nor
+    `interpreter_constraints` is specified) then the returned iterator will be empty. However, if
+    there are constraints specified, the returned iterator, although emtpy, will raise
+    `UnsatisfiableInterpreterConstraintsError` to provide information about any found interpreters
+    that did not match all the constraints.
+    """
 
-    interpreter = try_create(pex_python)
-    if interpreter:
-        # If the target interpreter specified in PEX_PYTHON is an existing absolute path - use it.
-        yield interpreter
-    else:
-        # Otherwise scan the PATH for matches:
-        try_paths = OrderedSet(
-            os.path.realpath(os.path.join(directory, pex_python))
-            for directory in os.getenv("PATH", "").split(os.pathsep)
+    _valid_path = None  # type: Optional[PathFilter]
+    if valid_basenames:
+        _valid_basenames = frozenset(cast("Iterable[str]", valid_basenames))
+        _valid_path = (
+            lambda interpreter_path: os.path.basename(interpreter_path) in _valid_basenames
         )
 
-        # Prefer the current interpreter if present in the `path`.
-        current_interpreter = PythonInterpreter.get()
-        if current_interpreter.binary in try_paths:
-            try_paths.remove(current_interpreter.binary)
-            yield current_interpreter
-
-        for try_path in try_paths:
-            interpreter = try_create(try_path)
-            if interpreter:
-                yield interpreter
-
-
-def iter_compatible_interpreters(path=None, compatibility_constraints=None):
-    """Find all compatible interpreters on the system within the supplied constraints and use path
-    if it is set.
-
-    If not, fall back to interpreters on $PATH.
-    """
-
     def _iter_interpreters():
+        # type: () -> Iterator[InterpreterOrError]
         seen = set()
 
-        paths = None
-        current_interpreter = PythonInterpreter.get()
+        paths = None  # type: Optional[MutableSet[str]]
         if path:
             paths = OrderedSet(os.path.realpath(p) for p in path.split(os.pathsep))
 
-            # Prefer the current interpreter if present on the `path`.
-            candidate_paths = frozenset(
-                (current_interpreter.binary, os.path.dirname(current_interpreter.binary))
-            )
-            candidate_paths_in_path = candidate_paths.intersection(paths)
-            if candidate_paths_in_path:
-                for p in candidate_paths_in_path:
-                    paths.remove(p)
+        current_interpreter = PythonInterpreter.get()
+        if not _valid_path or _valid_path(current_interpreter.binary):
+            if paths:
+                # Prefer the current interpreter if present on the `path`.
+                candidate_paths = frozenset(
+                    (current_interpreter.binary, os.path.dirname(current_interpreter.binary))
+                )
+                candidate_paths_in_path = candidate_paths.intersection(paths)
+                if candidate_paths_in_path:
+                    for p in candidate_paths_in_path:
+                        paths.remove(p)
+                    seen.add(current_interpreter)
+                    yield current_interpreter
+            else:
+                # We may have been invoked with a specific interpreter, make sure our sys.executable is
+                # included as a candidate in this case.
                 seen.add(current_interpreter)
                 yield current_interpreter
-        else:
-            # We may have been invoked with a specific interpreter, make sure our sys.executable is
-            # included as a candidate in this case.
-            seen.add(current_interpreter)
-            yield current_interpreter
 
-        for interp in PythonInterpreter.iter_candidates(paths=paths):
+        for interp in PythonInterpreter.iter_candidates(paths=paths, path_filter=_valid_path):
             if interp not in seen:
                 seen.add(interp)
                 yield interp
 
-    return _compatible_interpreters_iter(
-        _iter_interpreters(), compatibility_constraints=compatibility_constraints
-    )
+    def _valid_interpreter(interp_or_error):
+        # type: (InterpreterOrError) -> bool
+        if not isinstance(interp_or_error, PythonInterpreter):
+            return False
+
+        if not interpreter_constraints:
+            return True
+
+        interp = cast(PythonInterpreter, interp_or_error)
+
+        if any(
+            interp.identity.matches(interpreter_constraint)
+            for interpreter_constraint in interpreter_constraints
+        ):
+            TRACER.log(
+                "Constraints on interpreters: {}, Matching Interpreter: {}".format(
+                    interpreter_constraints, interp.binary
+                ),
+                V=3,
+            )
+            return True
 
+        return False
 
-def _matched_interpreters_iter(interpreters_iter, constraints):
-    candidates = []
-    failures = []
+    candidates = []  # type: List[PythonInterpreter]
+    failures = []  # type: List[InterpreterIdentificationError]
     found = False
 
-    for interpreter in interpreters_iter:
-        if isinstance(interpreter, PythonInterpreter):
+    for interpreter_or_error in _iter_interpreters():
+        if isinstance(interpreter_or_error, PythonInterpreter):
+            interpreter = cast(PythonInterpreter, interpreter_or_error)
             candidates.append(interpreter)
-            if any(interpreter.identity.matches(filt) for filt in constraints):
-                TRACER.log(
-                    "Constraints on interpreters: %s, Matching Interpreter: %s"
-                    % (constraints, interpreter.binary),
-                    V=3,
-                )
+            if _valid_interpreter(interpreter_or_error):
                 found = True
                 yield interpreter
         else:
-            failures.append(interpreter)
-
-    if not found:
+            error = cast("InterpreterIdentificationError", interpreter_or_error)
+            failures.append(error)
+
+    if not found and (interpreter_constraints or valid_basenames):
+        constraints = []  # type: List[str]
+        if interpreter_constraints:
+            constraints.append("Version matches {}".format(" or ".join(interpreter_constraints)))
+        if valid_basenames:
+            constraints.append("Basename is {}".format(" or ".join(valid_basenames)))
         raise UnsatisfiableInterpreterConstraintsError(constraints, candidates, failures)
 
 
-def _compatible_interpreters_iter(interpreters_iter, compatibility_constraints=None):
-    if compatibility_constraints:
-        for interpreter in _matched_interpreters_iter(interpreters_iter, compatibility_constraints):
-            yield interpreter
-    else:
-        for interpreter in interpreters_iter:
-            yield interpreter
-
-
-def _select_pex_python_interpreter(pex_python, compatibility_constraints=None):
-    compatible_interpreters_iter = _compatible_interpreters_iter(
-        _iter_pex_python(pex_python), compatibility_constraints=compatibility_constraints
+def _select_path_interpreter(
+    path=None,  # type: Optional[str]
+    valid_basenames=None,  # type: Optional[Tuple[str, ...]]
+    compatibility_constraints=None,  # type: Optional[Iterable[str]]
+):
+    # type: (...) -> Optional[PythonInterpreter]
+    candidate_interpreters_iter = iter_compatible_interpreters(
+        path=path,
+        valid_basenames=valid_basenames,
+        interpreter_constraints=compatibility_constraints,
     )
-    try:
-        return _select_interpreter(compatible_interpreters_iter)
-    except UnsatisfiableInterpreterConstraintsError as e:
-        die(
-            e.create_message(
-                "Failed to find a compatible PEX_PYTHON={pex_python}.".format(pex_python=pex_python)
-            )
-        )
-
-
-def _select_path_interpreter(path=None, compatibility_constraints=None):
-    compatible_interpreters_iter = iter_compatible_interpreters(
-        path=path, compatibility_constraints=compatibility_constraints
-    )
-    try:
-        return _select_interpreter(compatible_interpreters_iter)
-    except UnsatisfiableInterpreterConstraintsError as e:
-        die(
-            e.create_message(
-                "Failed to find compatible interpreter on path {path}.".format(
-                    path=path or os.getenv("PATH")
-                )
-            )
-        )
-
-
-def _select_interpreter(candidate_interpreters_iter):
-    current_interpreter = PythonInterpreter.get()
+    current_interpreter = PythonInterpreter.get()  # type: PythonInterpreter
     candidate_interpreters = []
     for interpreter in candidate_interpreters_iter:
         if current_interpreter == interpreter:
-            # Always prefer continuing with the current interpreter when possible.
+            # Always prefer continuing with the current interpreter when possible to avoid re-exec
+            # overhead.
             return current_interpreter
         else:
             candidate_interpreters.append(interpreter)
@@ -169,6 +177,7 @@ def _select_interpreter(candidate_interpreters_iter):
 
 
 def maybe_reexec_pex(compatibility_constraints=None):
+    # type: (Optional[Iterable[str]]) -> Union[None, NoReturn]
     """Handle environment overrides for the Python interpreter to use when executing this pex.
 
     This function supports interpreter filtering based on interpreter constraints stored in PEX-INFO
@@ -187,6 +196,7 @@ def maybe_reexec_pex(compatibility_constraints=None):
     """
 
     current_interpreter = PythonInterpreter.get()
+    target = None  # type: Optional[PythonInterpreter]
 
     # NB: Used only for tests.
     if "_PEX_EXEC_CHAIN" in os.environ:
@@ -198,7 +208,7 @@ def maybe_reexec_pex(compatibility_constraints=None):
     current_interpreter_blessed_env_var = "_PEX_SHOULD_EXIT_BOOTSTRAP_REEXEC"
     if os.environ.pop(current_interpreter_blessed_env_var, None):
         # We've already been here and selected an interpreter. Continue to execution.
-        return
+        return None
 
     from . import pex
 
@@ -217,9 +227,25 @@ def maybe_reexec_pex(compatibility_constraints=None):
                 ),
                 V=3,
             )
-            target = _select_pex_python_interpreter(
-                pex_python=ENV.PEX_PYTHON, compatibility_constraints=compatibility_constraints
-            )
+            try:
+                if os.path.isabs(ENV.PEX_PYTHON):
+                    target = _select_path_interpreter(
+                        path=ENV.PEX_PYTHON,
+                        compatibility_constraints=compatibility_constraints,
+                    )
+                else:
+                    target = _select_path_interpreter(
+                        valid_basenames=(os.path.basename(ENV.PEX_PYTHON),),
+                        compatibility_constraints=compatibility_constraints,
+                    )
+            except UnsatisfiableInterpreterConstraintsError as e:
+                die(
+                    e.create_message(
+                        "Failed to find a compatible PEX_PYTHON={pex_python}.".format(
+                            pex_python=ENV.PEX_PYTHON
+                        )
+                    )
+                )
         elif ENV.PEX_PYTHON_PATH or compatibility_constraints:
             TRACER.log(
                 "Using {path} constrained by {constraints}".format(
@@ -230,19 +256,49 @@ def maybe_reexec_pex(compatibility_constraints=None):
                 ),
                 V=3,
             )
-            target = _select_path_interpreter(
-                path=ENV.PEX_PYTHON_PATH, compatibility_constraints=compatibility_constraints
-            )
+            try:
+                target = _select_path_interpreter(
+                    path=ENV.PEX_PYTHON_PATH, compatibility_constraints=compatibility_constraints
+                )
+            except UnsatisfiableInterpreterConstraintsError as e:
+                die(
+                    e.create_message(
+                        "Failed to find compatible interpreter on path {path}.".format(
+                            path=ENV.PEX_PYTHON_PATH or os.getenv("PATH")
+                        )
+                    )
+                )
         elif pythonpath is None:
             TRACER.log(
                 "Using the current interpreter {} since no constraints have been specified and "
                 "PYTHONPATH is not set.".format(sys.executable),
                 V=3,
             )
-            return
+            return None
         else:
             target = current_interpreter
 
+    if not target:
+        # N.B.: This can only happen when PEX_PYTHON_PATH is set and compatibility_constraints is
+        # not set, but we handle all constraints generally for sanity sake.
+        constraints = []
+        if ENV.PEX_PYTHON:
+            constraints.append("PEX_PYTHON={}".format(ENV.PEX_PYTHON))
+        if ENV.PEX_PYTHON_PATH:
+            constraints.append("PEX_PYTHON_PATH={}".format(ENV.PEX_PYTHON_PATH))
+        if compatibility_constraints:
+            constraints.extend(
+                "--interpreter-constraint={}".format(compatibility_constraint)
+                for compatibility_constraint in compatibility_constraints
+            )
+
+        die(
+            "Failed to find an appropriate Python interpreter.\n"
+            "\n"
+            "Although the current interpreter is {python}, the following constraints exclude it:\n"
+            "  {constraints}".format(python=sys.executable, constraints="\n  ".join(constraints))
+        )
+
     os.environ.pop("PEX_PYTHON", None)
     os.environ.pop("PEX_PYTHON_PATH", None)
 
@@ -251,7 +307,7 @@ def maybe_reexec_pex(compatibility_constraints=None):
             "Using the current interpreter {} since it matches constraints and "
             "PYTHONPATH is not set.".format(sys.executable)
         )
-        return
+        return None
 
     target_binary = target.binary
     cmdline = [target_binary] + sys.argv
@@ -262,7 +318,7 @@ def maybe_reexec_pex(compatibility_constraints=None):
         "PEX_PYTHON={pex_python!r}, "
         "PEX_PYTHON_PATH={pex_python_path!r}, "
         "COMPATIBILITY_CONSTRAINTS={compatibility_constraints!r}"
-        '{pythonpath}"'.format(
+        "{pythonpath}".format(
             cmdline=" ".join(cmdline),
             python=sys.executable,
             pex_python=ENV.PEX_PYTHON,
@@ -281,15 +337,16 @@ def maybe_reexec_pex(compatibility_constraints=None):
 
 
 def _bootstrap(entry_point):
-    from .pex_info import PexInfo
-
-    pex_info = PexInfo.from_pex(entry_point)
+    # type: (str) -> PexInfo
+    pex_info = PexInfo.from_pex(entry_point)  # type: PexInfo
     pex_info.update(PexInfo.from_env())
     pex_warnings.configure_warnings(pex_info, ENV)
     return pex_info
 
 
+# NB: This helper is used by the PEX bootstrap __main__.py code.
 def bootstrap_pex(entry_point):
+    # type: (str) -> None
     pex_info = _bootstrap(entry_point)
     maybe_reexec_pex(pex_info.interpreter_constraints)
 
@@ -301,14 +358,16 @@ def bootstrap_pex(entry_point):
 # NB: This helper is used by third party libs - namely https://github.com/wickman/lambdex.
 # TODO(John Sirois): Kill once https://github.com/wickman/lambdex/issues/5 is resolved.
 def is_compressed(entry_point):
-    from .pex_info import PexInfo
-
+    # type: (str) -> bool
     return os.path.exists(entry_point) and not os.path.exists(
         os.path.join(entry_point, PexInfo.PATH)
     )
 
 
+# NB: This helper is used by third party libs like https://github.com/wickman/lambdex and
+# https://github.com/kwlzn/pyuwsgi_pex.
 def bootstrap_pex_env(entry_point):
+    # type: (str) -> None
     """Bootstrap the current runtime environment using a given pex."""
     pex_info = _bootstrap(entry_point)
 


Test patch:
diff --git a/tests/test_pex_bootstrapper.py b/tests/test_pex_bootstrapper.py
--- a/tests/test_pex_bootstrapper.py
+++ b/tests/test_pex_bootstrapper.py
@@ -14,15 +14,22 @@
 from pex.typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from typing import AnyStr, List
+    from typing import AnyStr, Iterable, List, Optional
 
 
-def find_interpreters(path, *constraints):
-    # type: (List[str], *str) -> List[AnyStr]
+def basenames(*paths):
+    # type: (*str) -> Iterable[str]
+    return [os.path.basename(p) for p in paths]
+
+
+def find_interpreters(path, valid_basenames=None, constraints=None):
+    # type: (Iterable[str], Optional[Iterable[str]], Optional[Iterable[str]]) -> List[AnyStr]
     return [
         interp.binary
         for interp in iter_compatible_interpreters(
-            path=os.pathsep.join(path), compatibility_constraints=constraints
+            path=os.pathsep.join(path),
+            valid_basenames=valid_basenames,
+            interpreter_constraints=constraints,
         )
     ]
 
@@ -34,27 +41,27 @@ def test_find_compatible_interpreters():
     py36 = ensure_python_interpreter(PY36)
     path = [py27, py35, py36]
 
-    assert [py35, py36] == find_interpreters(path, ">3")
-    assert [py27] == find_interpreters(path, "<3")
+    assert [py35, py36] == find_interpreters(path, constraints=[">3"])
+    assert [py27] == find_interpreters(path, constraints=["<3"])
 
-    assert [py36] == find_interpreters(path, ">{}".format(PY35))
-    assert [py35] == find_interpreters(path, ">{}, <{}".format(PY27, PY36))
-    assert [py36] == find_interpreters(path, ">=3.6")
+    assert [py36] == find_interpreters(path, constraints=[">{}".format(PY35)])
+    assert [py35] == find_interpreters(path, constraints=[">{}, <{}".format(PY27, PY36)])
+    assert [py36] == find_interpreters(path, constraints=[">=3.6"])
 
     with pytest.raises(UnsatisfiableInterpreterConstraintsError):
-        find_interpreters(path, "<2")
+        find_interpreters(path, constraints=["<2"])
 
     with pytest.raises(UnsatisfiableInterpreterConstraintsError):
-        find_interpreters(path, ">4")
+        find_interpreters(path, constraints=[">4"])
 
     with pytest.raises(UnsatisfiableInterpreterConstraintsError):
-        find_interpreters(path, ">{}, <{}".format(PY27, PY35))
+        find_interpreters(path, constraints=[">{}, <{}".format(PY27, PY35)])
 
     # All interpreters on PATH including whatever interpreter is currently running.
     all_known_interpreters = set(PythonInterpreter.all())
     all_known_interpreters.add(PythonInterpreter.get())
 
-    interpreters = set(iter_compatible_interpreters(compatibility_constraints=["<3"]))
+    interpreters = set(iter_compatible_interpreters(interpreter_constraints=["<3"]))
     i_rendered = "\n      ".join(sorted(map(repr, interpreters)))
     aki_rendered = "\n      ".join(sorted(map(repr, all_known_interpreters)))
     assert interpreters.issubset(all_known_interpreters), dedent(
@@ -72,8 +79,77 @@ def test_find_compatible_interpreters():
 
 def test_find_compatible_interpreters_none():
     # type: () -> None
-    with pytest.raises(UnsatisfiableInterpreterConstraintsError):
-        find_interpreters([os.path.devnull], ">2")
+    assert [] == find_interpreters([os.path.devnull])
+
+
+def test_find_compatible_interpreters_none_with_valid_basenames():
+    # type: () -> None
+    py27 = ensure_python_interpreter(PY27)
+    py35 = ensure_python_interpreter(PY35)
+    path = [py27, py35]
+
+    with pytest.raises(UnsatisfiableInterpreterConstraintsError) as exec_info:
+        find_interpreters(path, valid_basenames=["python3.6"])
+
+    exception_message = str(exec_info.value)
+    assert py27 not in exception_message
+    assert py35 not in exception_message
+
+
+def test_find_compatible_interpreters_none_with_constraints():
+    # type: () -> None
+    py27 = ensure_python_interpreter(PY27)
+    py35 = ensure_python_interpreter(PY35)
+    path = [py27, py35]
+
+    with pytest.raises(UnsatisfiableInterpreterConstraintsError) as exec_info:
+        find_interpreters(path, constraints=[">=3.6"])
+
+    exception_message = str(exec_info.value)
+    assert py27 in exception_message
+    assert py35 in exception_message
+    assert ">=3.6" in exception_message
+
+
+def test_find_compatible_interpreters_none_with_valid_basenames_and_constraints():
+    # type: () -> None
+    py27 = ensure_python_interpreter(PY27)
+    py35 = ensure_python_interpreter(PY35)
+    path = [py27, py35]
+
+    with pytest.raises(UnsatisfiableInterpreterConstraintsError) as exec_info:
+        find_interpreters(path, valid_basenames=basenames(py27), constraints=[">=3.6"])
+
+    exception_message = str(exec_info.value)
+    assert py27 in exception_message
+    assert py35 not in exception_message
+    assert os.path.basename(py27) in exception_message, exception_message
+    assert ">=3.6" in exception_message
+
+
+def test_find_compatible_interpreters_with_valid_basenames():
+    # type: () -> None
+    py27 = ensure_python_interpreter(PY27)
+    py35 = ensure_python_interpreter(PY35)
+    py36 = ensure_python_interpreter(PY36)
+    path = [py27, py35, py36]
+
+    assert [py35] == find_interpreters(path, valid_basenames=basenames(py35))
+    assert [py27, py36] == find_interpreters(
+        path, valid_basenames=basenames(*reversed([py27, py36]))
+    )
+
+
+def test_find_compatible_interpreters_with_valid_basenames_and_constraints():
+    # type: () -> None
+    py27 = ensure_python_interpreter(PY27)
+    py35 = ensure_python_interpreter(PY35)
+    py36 = ensure_python_interpreter(PY36)
+    path = [py27, py35, py36]
+
+    assert [py35] == find_interpreters(
+        path, valid_basenames=basenames(py27, py35), constraints=[">=3"]
+    )
 
 
 def test_find_compatible_interpreters_bias_current():