Skip to content

Commit

Permalink
Fix runtime interpreter selection failure message.
Browse files Browse the repository at this point in the history
The fix for pex-tool#1020 did not add language for the new resolve check
failures.
  • Loading branch information
jsirois committed May 18, 2022
1 parent d9e6caf commit a9fc5cb
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 33 deletions.
18 changes: 17 additions & 1 deletion pex/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pex.typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
from typing import AnyStr, Optional, Text, Tuple, Type
from typing import AnyStr, Callable, Optional, Text, Tuple, Type


try:
Expand Down Expand Up @@ -172,3 +172,19 @@ def is_valid_python_identifier(text):
# why it's nt in the stdlib.
# See: https://docs.python.org/2.7/reference/lexical_analysis.html#identifiers
return re.match(r"^[_a-zA-Z][_a-zA-Z0-9]*$", text) is not None


if PY2:

def indent(
text, # type: Text
prefix, # type: Text
predicate=None, # type: Optional[Callable[[Text], bool]]
):
add_prefix = predicate if predicate else lambda line: bool(line.strip())
return "".join(
prefix + line if add_prefix(line) else line for line in text.splitlines(True)
)

else:
from textwrap import indent as indent
12 changes: 7 additions & 5 deletions pex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,12 @@ def render_message(self, target):
# type: (Target) -> str
return (
"The wheel tags for {dist} are {wheel_tags} which do not match the supported tags of "
"{target}:\n{supported_tags}".format(
"{target}:\n{tag}\n... {count} more ...".format(
dist=self.dist,
wheel_tags=", ".join(map(str, self.wheel_tags)),
target=target,
supported_tags="\n".join(map(str, target.supported_tags)),
tag=target.supported_tags[0],
count=len(target.supported_tags) - 1,
)
)

Expand Down Expand Up @@ -442,13 +443,14 @@ def _root_requirements_iter(self, reqs):
# We've winnowed down reqs_by_key to just those requirements whose environment
# markers apply; so, we should always have an available distribution.
message = (
"A distribution for {project_name} could not be resolved in this "
"environment.".format(project_name=project_name)
"A distribution for {project_name} could not be resolved for {target}.".format(
project_name=project_name, target=self._target
)
)
unavailable_dists = self._unavailable_dists_by_project_name.get(project_name)
if unavailable_dists:
message += (
"Found {count} {distributions} for {project_name} that do not apply:\n"
"\nFound {count} {distributions} for {project_name} that do not apply:\n"
"{unavailable_dists}".format(
count=len(unavailable_dists),
distributions=pluralize(unavailable_dists, "distribution"),
Expand Down
10 changes: 8 additions & 2 deletions pex/interpreter_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import itertools

from pex.compatibility import indent
from pex.enum import Enum
from pex.interpreter import PythonInterpreter
from pex.orderedset import OrderedSet
Expand Down Expand Up @@ -111,8 +112,13 @@ def create_message(self, preamble=None):
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))
"\n{constraints}"
).format(
qualifier=qualifier,
constraints="\n\n".join(
indent(constraint, " ") for constraint in self.constraints
),
)

problems = "\n\n".join(msg for msg in (failures_message, constraints_message) if msg)
if problems:
Expand Down
52 changes: 28 additions & 24 deletions pex/pex_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def interpreter_constraints(self):
return self.pex_info.interpreter_constraints

def test_resolve(self, interpreter):
# type: (PythonInterpreter) -> bool
# type: (PythonInterpreter) -> Optional[ResolveError]
"""Checks if `interpreter` can resolve all required distributions for the PEX under test."""
with TRACER.timed(
"Testing {python} can resolve PEX at {pex}".format(
Expand All @@ -69,9 +69,9 @@ def test_resolve(self, interpreter):
)
try:
pex_environment.resolve()
return True
return None
except ResolveError as e:
return False
return e


# TODO(John Sirois): Move this to interpreter_constraints.py. As things stand, both pex/bin/pex.py
Expand Down Expand Up @@ -144,14 +144,10 @@ def _iter_interpreters():
seen.add(interp)
yield interp

def _valid_interpreter(interp_or_error):
# type: (InterpreterOrError) -> bool
if not isinstance(interp_or_error, PythonInterpreter):
return False

interp = interp_or_error
def _valid_interpreter(interp):
# type: (PythonInterpreter) -> Optional[ResolveError]
if not interpreter_constraints:
return interpreter_test.test_resolve(interp) if interpreter_test else True
return interpreter_test.test_resolve(interp) if interpreter_test else None

if any(
interp.identity.matches(interpreter_constraint)
Expand All @@ -163,34 +159,42 @@ def _valid_interpreter(interp_or_error):
),
V=3,
)
return interpreter_test.test_resolve(interp) if interpreter_test else True
return interpreter_test.test_resolve(interp) if interpreter_test else None

return False
return None

candidates = [] # type: List[PythonInterpreter]
failures = [] # type: List[InterpreterIdentificationError]
resolve_errors = [] # type: List[ResolveError]
identification_failures = [] # type: List[InterpreterIdentificationError]
found = False

for interpreter_or_error in _iter_interpreters():
if isinstance(interpreter_or_error, PythonInterpreter):
interpreter = interpreter_or_error
candidates.append(interpreter)
if _valid_interpreter(interpreter):
error = _valid_interpreter(interpreter)
if error:
resolve_errors.append(error)
else:
found = True
yield interpreter
else:
error = cast("InterpreterIdentificationError", interpreter_or_error)
failures.append(error)
identification_failures.append(interpreter_or_error)

if not found and (interpreter_constraints or valid_basenames):
if not found:
constraints = [] # type: List[str]
if interpreter_constraints:
constraints.append(
"Version matches {}".format(" or ".join(map(str, interpreter_constraints)))
)
if valid_basenames:
constraints.append("Basename is {}".format(" or ".join(valid_basenames)))
raise UnsatisfiableInterpreterConstraintsError(constraints, candidates, failures)
if resolve_errors:
constraints.extend(str(resolve_error) for resolve_error in resolve_errors)
else:
if interpreter_constraints:
constraints.append(
"Version matches {}".format(" or ".join(map(str, interpreter_constraints)))
)
if valid_basenames:
constraints.append("Basename is {}".format(" or ".join(valid_basenames)))
raise UnsatisfiableInterpreterConstraintsError(
constraints, candidates, identification_failures
)


def _select_path_interpreter(
Expand Down
54 changes: 54 additions & 0 deletions tests/integration/test_pex_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,57 @@ def test_boot_compatible_issue_1020_ic_min_compatible_build_time_hole(tmpdir):
str(output.decode("ascii").strip())
).resolve_base_interpreter()
)


def test_boot_resolve_fail(
tmpdir, # type: Any
py27, # type: PythonInterpreter
py37, # type: PythonInterpreter
py310, # type: PythonInterpreter
):
# type: (...) -> None

pex = os.path.join(str(tmpdir), "pex")
run_pex_command(args=["--python", py37.binary, "psutil==5.9.0", "-o", pex]).assert_success()

pex_python_path = ":".join((py27.binary, py310.binary))
process = subprocess.Popen(
args=[py27.binary, pex, "-c", ""],
env=make_env(PEX_PYTHON_PATH=pex_python_path),
stderr=subprocess.PIPE,
)
_, stderr = process.communicate()
assert 0 != process.returncode
error = stderr.decode("utf-8").strip()
pattern = re.compile(
r"^Failed to find compatible interpreter on path {pex_python_path}.\n"
r"\n"
r"Examined the following interpreters:\n"
r"1\.\)\s+{py27_exe} {py27_req}\n"
r"2\.\)\s+{py310_exe} {py310_req}\n"
r"\n"
r"No interpreter compatible with the requested constraints was found:\n"
r"\n"
r" A distribution for psutil could not be resolved for {py27_exe}.\n"
r" Found 1 distribution for psutil that do not apply:\n"
r" 1\.\) The wheel tags for psutil 5\.9\.0 are .+ which do not match the supported tags "
r"of {py27_exe}:\n"
r" cp27-cp27.+\n"
r" ... \d+ more ...\n"
r"\n"
r" A distribution for psutil could not be resolved for {py310_exe}.\n"
r" Found 1 distribution for psutil that do not apply:\n"
r" 1\.\) The wheel tags for psutil 5\.9\.0 are .+ which do not match the supported tags "
r"of {py310_exe}:\n"
r" cp310-cp310-.+\n"
r" ... \d+ more ...".format(
pex_python_path=re.escape(pex_python_path),
py27_exe=py27.binary,
py27_req=py27.identity.requirement,
py310_exe=py310.binary,
py310_req=py310.identity.requirement,
),
)
assert pattern.match(error), "Got error:\n{error}\n\nExpected pattern\n{pattern}".format(
error=error, pattern=pattern.pattern
)
14 changes: 13 additions & 1 deletion tests/test_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from pex.compatibility import PY3, to_bytes, to_unicode
from pex.compatibility import PY3, indent, to_bytes, to_unicode

unicode_string = (str,) if PY3 else (unicode,) # type: ignore[name-defined]

Expand Down Expand Up @@ -32,3 +32,15 @@ def test_to_unicode():
for bad_value in (123, None):
with pytest.raises(ValueError):
to_unicode(bad_value) # type: ignore[type-var]


def test_indent():
# type: () -> None
assert " line1" == indent("line1", " ")

assert " line1\n line2" == indent("line1\nline2", " ")
assert " line1\n line2\n" == indent("line1\nline2\n", " ")

assert " line1\n\n line3" == indent("line1\n\nline3", " ")
assert " line1\n \n line3" == indent("line1\n \nline3", " ")
assert " line1\n \n line3" == indent("line1\n\nline3", " ", lambda line: True)

0 comments on commit a9fc5cb

Please sign in to comment.