Skip to content

Commit

Permalink
OR interpreter constraints when multiple given (#678)
Browse files Browse the repository at this point in the history
## Problem
Resolves #655. 

We would expect a list of constraints, such as `['CPython>=2.7,<3', 'CPython>=3.4']`, to OR the separate list entries, i.e. allow an interpreter that satisfies any of these distinct requirement strings. Instead, we always AND lists of constraints.

## Solution
Always `OR` a list of interpreter constraints. If the user wants `AND`, they may do so within the single requirement string via `,`, e.g. `'CPython>=2.7,<3'`.
  • Loading branch information
Eric-Arellano committed Mar 10, 2019
1 parent dbf92a9 commit f9327f9
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 30 deletions.
8 changes: 4 additions & 4 deletions pex/bin/pex.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,10 @@ def configure_clp_pex_environment(parser):
default=[],
type='str',
action='append',
help='A constraint that determines the interpreter compatibility for '
'this pex, using the Requirement-style format, e.g. "CPython>=3", or ">=2.7" '
'for requirements agnostic to interpreter class. This option can be passed multiple '
'times.')
help='Constrain the selected Python interpreter. Specify with Requirement-style syntax, '
'e.g. "CPython>=2.7,<3" (A CPython interpreter with version >=2.7 AND version <3) '
'or "PyPy" (A pypy interpreter of any version). This argument may be repeated multiple '
'times to OR the constraints.')

group.add_option(
'--rcfile',
Expand Down
15 changes: 6 additions & 9 deletions pex/interpreter_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,18 @@ def validate_constraints(constraints):
die("Compatibility requirements are not formatted properly: %s" % str(e))


def matched_interpreters(interpreters, constraints, meet_all_constraints=False):
"""Given some filters, yield any interpreter that matches at least one of them, or all of them
if meet_all_constraints is set to True.
def matched_interpreters(interpreters, constraints):
"""Given some filters, yield any interpreter that matches at least one of them.
:param interpreters: a list of PythonInterpreter objects for filtering
:param constraints: A sequence of strings that constrain the interpreter compatibility for this
pex, using the Requirement-style format, e.g. ``'CPython>=3', or just ['>=2.7','<3']``
for requirements agnostic to interpreter class.
:param meet_all_constraints: whether to match against all filters.
Defaults to matching interpreters that match at least one filter.
pex. Each string uses the Requirement-style format, e.g. 'CPython>=3' or '>=2.7,<3' for
requirements agnostic to interpreter class. Multiple requirement strings may be combined
into a list to OR the constraints, such as ['CPython>=2.7,<3', 'CPython>=3.4'].
:return interpreter: returns a generator that yields compatible interpreters
"""
check = all if meet_all_constraints else any
for interpreter in interpreters:
if check(interpreter.identity.matches(filt) for filt in constraints):
if any(interpreter.identity.matches(filt) for filt in constraints):
TRACER.log("Constraints on interpreters: %s, Matching Interpreter: %s"
% (constraints, interpreter.binary), V=3)
yield interpreter
9 changes: 6 additions & 3 deletions pex/pex_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ def find_compatible_interpreters(pex_python_path, compatibility_constraints):
# get all qualifying interpreters found in $PATH
interpreters = PythonInterpreter.all()

return list(matched_interpreters(
interpreters, compatibility_constraints, meet_all_constraints=True))
return list(
matched_interpreters(interpreters, compatibility_constraints)
if compatibility_constraints
else interpreters
)


def _select_pex_python_interpreter(target_python, compatibility_constraints):
Expand All @@ -96,7 +99,7 @@ def _select_pex_python_interpreter(target_python, compatibility_constraints):
die('Failed to find interpreter specified by PEX_PYTHON: %s' % target)
if compatibility_constraints:
pi = PythonInterpreter.from_binary(target)
if not list(matched_interpreters([pi], compatibility_constraints, meet_all_constraints=True)):
if not list(matched_interpreters([pi], compatibility_constraints)):
die('Interpreter specified by PEX_PYTHON (%s) is not compatible with specified '
'interpreter constraints: %s' % (target, str(compatibility_constraints)))
if not os.path.exists(target):
Expand Down
38 changes: 24 additions & 14 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,12 @@ def test_interpreter_constraints_to_pex_info_py2():
# target python 2
pex_out_path = os.path.join(output_dir, 'pex_py2.pex')
res = run_pex_command(['--disable-cache',
'--interpreter-constraint=>=2.7',
'--interpreter-constraint=<3',
'--interpreter-constraint=>=2.7,<3',
'--interpreter-constraint=>=3.5',
'-o', pex_out_path])
res.assert_success()
pex_info = get_pex_info(pex_out_path)
assert set(['>=2.7', '<3']) == set(pex_info.interpreter_constraints)
assert {'>=2.7,<3', '>=3.5'} == set(pex_info.interpreter_constraints)


@pytest.mark.skipif(IS_PYPY)
Expand All @@ -359,12 +359,26 @@ def test_interpreter_resolution_with_constraint_option():
with temporary_dir() as output_dir:
pex_out_path = os.path.join(output_dir, 'pex1.pex')
res = run_pex_command(['--disable-cache',
'--interpreter-constraint=>=2.7',
'--interpreter-constraint=<3',
'--interpreter-constraint=>=2.7,<3',
'-o', pex_out_path])
res.assert_success()
pex_info = get_pex_info(pex_out_path)
assert set(['>=2.7', '<3']) == set(pex_info.interpreter_constraints)
assert ['>=2.7,<3'] == pex_info.interpreter_constraints
assert pex_info.build_properties['version'][0] < 3


def test_interpreter_resolution_with_multiple_constraint_options():
with temporary_dir() as output_dir:
pex_out_path = os.path.join(output_dir, 'pex1.pex')
res = run_pex_command(['--disable-cache',
'--interpreter-constraint=>=2.7,<3',
# Add a constraint that's impossible to satisfy. Because multiple
# constraints OR, the interpeter should still resolve to Python 2.7.
'--interpreter-constraint=>=500',
'-o', pex_out_path])
res.assert_success()
pex_info = get_pex_info(pex_out_path)
assert {'>=2.7,<3', '>=500'} == set(pex_info.interpreter_constraints)
assert pex_info.build_properties['version'][0] < 3


Expand All @@ -388,8 +402,7 @@ def test_interpreter_resolution_with_pex_python_path():
pex_out_path = os.path.join(td, 'pex.pex')
res = run_pex_command(['--disable-cache',
'--rcfile=%s' % pexrc_path,
'--interpreter-constraint=%s' % interpreter_constraint1,
'--interpreter-constraint=%s' % interpreter_constraint2,
'--interpreter-constraint=%s,%s' % (interpreter_constraint1, interpreter_constraint2),
'-o', pex_out_path])
res.assert_success()

Expand Down Expand Up @@ -420,8 +433,7 @@ def test_interpreter_resolution_pex_python_path_precedence_over_pex_python():
pex_out_path = os.path.join(td, 'pex.pex')
res = run_pex_command(['--disable-cache',
'--rcfile=%s' % pexrc_path,
'--interpreter-constraint=>3',
'--interpreter-constraint=<3.8',
'--interpreter-constraint=>3,<3.8',
'-o', pex_out_path])
res.assert_success()

Expand Down Expand Up @@ -515,8 +527,7 @@ def test_pex_python():
pex_out_path = os.path.join(td, 'pex.pex')
res = run_pex_command(['--disable-cache',
'--rcfile=%s' % pexrc_path,
'--interpreter-constraint=>3',
'--interpreter-constraint=<3.8',
'--interpreter-constraint=>3,<3.8',
'-o', pex_out_path],
env=env)
res.assert_success()
Expand All @@ -536,8 +547,7 @@ def test_pex_python():
pex_out_path = os.path.join(td, 'pex2.pex')
res = run_pex_command(['--disable-cache',
'--rcfile=%s' % pexrc_path,
'--interpreter-constraint=>3',
'--interpreter-constraint=<3.8',
'--interpreter-constraint=>3,<3.8',
'-o', pex_out_path],
env=env)
res.assert_success()
Expand Down

0 comments on commit f9327f9

Please sign in to comment.