Skip to content

Commit

Permalink
Add runtime check for valid Python interpreter (#7365)
Browse files Browse the repository at this point in the history
### Problem
Now that we'll soon allow running Pants with more than Python 2.7 for every day users, we should eagerly ensure that they are using a supported Python version. This is necessary because Pants is typically ran as a script, rather than as a library, and the script may be ran with whichever interpreter the user chooses.

Even though the setup repo is supposed to ensure people use a valid interpreter, it is easy to get around this with `PYTHON=3.5 ./pants` or with putting `pants_engine_python_version: 3.5` in their `pants.ini`. Both these scenarios are totally valid to do, and we should not teach the setup repo or `--pants-engine-python-version` what are valid options, because the former is easy to fall out of sync with users and the latter is easy to circumvent.

While Pants _might_ work with something like 3.5, we should at least warn users they're treading into dangerous waters. If they want to proceed by setting an env var bypass, all power to them. But for the sake of UX we should at least give the warning, rather than relying on strange bugs and `SyntaxError`s to enforce using the correct version.

### Solution
Add a runtime check to `pants_loader.py` with a human readable error message. Allow skipping the check with `PANTS_IGNORE_UNSUPPORTED_PYTHON_INTERPRETER`.

Add unit tests for this all.

### Result
When running with anything other than 2.7 or 3.6+, Pants will error out.

Once we drop Py2, we can change this check to 3.6+.
  • Loading branch information
Eric-Arellano committed Mar 14, 2019
1 parent b3ab38c commit 5879cf1
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 2 deletions.
34 changes: 33 additions & 1 deletion src/python/pants/bin/pants_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import importlib
import locale
import os
import sys
import warnings
from builtins import object
from textwrap import dedent
Expand All @@ -19,10 +20,19 @@ class PantsLoader(object):
DEFAULT_ENTRYPOINT = 'pants.bin.pants_exe:main'

ENCODING_IGNORE_ENV_VAR = 'PANTS_IGNORE_UNRECOGNIZED_ENCODING'
INTERPRETER_IGNORE_ENV_VAR = 'PANTS_IGNORE_UNSUPPORTED_PYTHON_INTERPRETER'

class InvalidLocaleError(Exception):
"""Raised when a valid locale can't be found."""

class InvalidInterpreter(Exception):
"""Raised when trying to run Pants with an unsupported Python version."""

@staticmethod
def is_supported_interpreter(major_version, minor_version):
return (major_version == 2 and minor_version == 7) \
or (major_version == 3 and minor_version >= 6)

@staticmethod
def setup_warnings():
# We want to present warnings to the user, set this up before importing any of our own code,
Expand All @@ -49,7 +59,7 @@ def ensure_locale(cls):
# libraries called by Pants may fail with more obscure errors.
encoding = locale.getpreferredencoding()
if encoding.lower() != 'utf-8' and os.environ.get(cls.ENCODING_IGNORE_ENV_VAR, None) is None:
raise cls.InvalidLocaleError(dedent("""\
raise cls.InvalidLocaleError(dedent("""
Your system's preferred encoding is `{}`, but Pants requires `UTF-8`.
Specifically, Python's `locale.getpreferredencoding()` must resolve to `UTF-8`.
Expand All @@ -62,6 +72,27 @@ def ensure_locale(cls):
""".format(encoding, cls.ENCODING_IGNORE_ENV_VAR)
))

@classmethod
def ensure_valid_interpreter(cls):
"""Runtime check that user is using a supported Python version."""
py_major, py_minor = sys.version_info[0:2]
if not PantsLoader.is_supported_interpreter(py_major, py_minor) and os.environ.get(cls.INTERPRETER_IGNORE_ENV_VAR, None) is None:
raise cls.InvalidInterpreter(dedent("""
You are trying to run Pants with Python {}.{}, which is unsupported.
Pants requires a Python 2.7 or a Python 3.6+ interpreter to be
discoverable on your PATH to run.
If you still get this error after ensuring at least one of these interpreters
is discoverable on your PATH, you may need to modify your build script
(e.g. `./pants`) to properly set up a virtual environment with the correct
interpreter. We recommend following our setup guide and using our setup script
as a starting point: https://www.pantsbuild.org/setup_repo.html.
Alternatively, you may bypass this error by setting the below environment variable.
{}=1
Note: we cannot guarantee consistent behavior with this bypass enabled.
""".format(py_major, py_minor, cls.INTERPRETER_IGNORE_ENV_VAR)))

@staticmethod
def determine_entrypoint(env_var, default):
return os.environ.pop(env_var, default)
Expand All @@ -78,6 +109,7 @@ def load_and_execute(entrypoint):
@classmethod
def run(cls):
cls.setup_warnings()
cls.ensure_valid_interpreter()
cls.ensure_locale()
entrypoint = cls.determine_entrypoint(cls.ENTRYPOINT_ENV_VAR, cls.DEFAULT_ENTRYPOINT)
cls.load_and_execute(entrypoint)
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/init/options_initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def create(cls, options_bootstrapper, build_configuration, init_subsystems=True)
raise BuildConfigurationError(
'Running Pants with a different Python interpreter version than requested. '
'You requested {}, but are running with {}.\n\n'
'Note that Pants cannot use the value you give for `--pants-engine-python-version` to '
'Note that Pants cannot use the value you give for `--pants-runtime-python-version` to '
'dynamically change the interpreter it uses, as it is too late for it to change once the program '
'is already running. Instead, your setup script (e.g. `./pants`) must configure which Python '
'interpreter and virtualenv to use. For example, the setup script we distribute '
Expand Down
11 changes: 11 additions & 0 deletions tests/python/pants_test/bin/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_tests(
sources='test_loader.py',
dependencies=[
'src/python/pants/bin',
'3rdparty/python:future',
'3rdparty/python:mock',
],
)

python_tests(
name='integration',
sources='test_loader_integration.py',
dependencies=[
'src/python/pants/bin',
'tests/python/pants_test:int-test',
Expand Down
34 changes: 34 additions & 0 deletions tests/python/pants_test/bin/test_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# coding=utf-8
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import, division, print_function, unicode_literals

import os
import sys
import unittest
from builtins import map, str

import mock

from pants.bin.pants_loader import PantsLoader


class LoaderTest(unittest.TestCase):

def test_is_supported_interpreter(self):
unsupported_versions = {(2, 6), (3, 3), (3, 4), (3, 5), (4, 0)}
supported_versions = {(2, 7), (3, 6), (3, 7), (3, 8)}
self.assertTrue(all(not PantsLoader.is_supported_interpreter(major, minor) for major, minor in unsupported_versions))
self.assertTrue(all(PantsLoader.is_supported_interpreter(major, minor) for major, minor in supported_versions))

def test_ensure_valid_interpreter(self):
current_interpreter_version = '.'.join(map(str, sys.version_info[0:2]))
bypass_env = PantsLoader.INTERPRETER_IGNORE_ENV_VAR
with mock.patch.object(PantsLoader, 'is_supported_interpreter', return_value=False):
with self.assertRaises(PantsLoader.InvalidInterpreter) as e:
PantsLoader.ensure_valid_interpreter()
self.assertIn(current_interpreter_version, str(e.exception))
self.assertIn(bypass_env, str(e.exception))
with mock.patch.dict(os.environ, {bypass_env: "1"}):
PantsLoader.ensure_valid_interpreter()

0 comments on commit 5879cf1

Please sign in to comment.