Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user to inject specific Pythons to try as step 1 during discovery #1526

Merged
merged 1 commit into from
Feb 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/1526.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``--discover`` (fallback to ``TOX_DISCOVER`` environment variable via path separator) to inject python executables
to try as first step of a discovery - note the executable still needs to match the environment by :user:`gaborbernat`.
8 changes: 8 additions & 0 deletions src/tox/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,14 @@ def tox_addoption(parser):
help="write a json file with detailed information "
"about all commands and results involved.",
)
parser.add_argument(
"--discover",
dest="discover",
nargs="+",
metavar="PATH",
help="for python discovery first try the python executables under these paths",
default=[],
)

# We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED.
parser.add_argument(
Expand Down
25 changes: 25 additions & 0 deletions src/tox/interpreters/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os

from tox.interpreters.py_spec import CURRENT, PythonSpec
from tox.interpreters.via_path import exe_spec


def base_discover(envconfig):
base_python = envconfig.basepython
spec = PythonSpec.from_name(base_python)

# 1. check passed in discover elements
discovers = envconfig.config.option.discover
if not discovers:
discovers = os.environ.get(str("TOX_DISCOVER"), "").split(os.pathsep)
for discover in discovers:
if os.path.exists(discover):
cur_spec = exe_spec(discover, envconfig.basepython)
if cur_spec is not None and cur_spec.satisfies(spec):
return spec, cur_spec.path

# 2. check current
if spec.name is not None and CURRENT.satisfies(spec):
return spec, CURRENT.path

return spec, None
18 changes: 8 additions & 10 deletions src/tox/interpreters/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

import tox

from .py_spec import CURRENT, PythonSpec
from .common import base_discover
from .via_path import check_with_path


@tox.hookimpl
def tox_get_python_executable(envconfig):
base_python = envconfig.basepython
spec = PythonSpec.from_name(base_python)
# first, check current
if spec.name is not None and CURRENT.satisfies(spec):
return CURRENT.path
# second check if the literal base python
candidates = [base_python]
# third check if the un-versioned name is good
if spec.name is not None and spec.name != base_python:
spec, path = base_discover(envconfig)
if path is not None:
return path
# 3. check if the literal base python
candidates = [envconfig.basepython]
# 4. check if the un-versioned name is good
if spec.name is not None and spec.name != envconfig.basepython:
candidates.append(spec.name)
return check_with_path(candidates, spec)
14 changes: 6 additions & 8 deletions src/tox/interpreters/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@

import tox

from ..py_spec import CURRENT, PythonSpec
from ..common import base_discover
from ..py_spec import CURRENT
from ..via_path import check_with_path


@tox.hookimpl
def tox_get_python_executable(envconfig):
base_python = envconfig.basepython
spec = PythonSpec.from_name(base_python)
# first, check current
if spec.name is not None and CURRENT.satisfies(spec):
return CURRENT.path

spec, path = base_discover(envconfig)
if path is not None:
return path
# second check if the py.exe has it (only for non path specs)
if spec.path is None:
py_exe = locate_via_pep514(spec)
Expand All @@ -25,7 +23,7 @@ def tox_get_python_executable(envconfig):
# third check if the literal base python is on PATH
candidates = [envconfig.basepython]
# fourth check if the name is on PATH
if spec.name is not None and spec.name != base_python:
if spec.name is not None and spec.name != envconfig.basepython:
candidates.append(spec.name)
# or check known locations
if spec.major is not None and spec.minor is not None:
Expand Down
20 changes: 15 additions & 5 deletions tests/unit/interpreters/test_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ def create_interpreters_instance():


@pytest.mark.skipif(tox.INFO.IS_PYPY, reason="testing cpython interpreter discovery")
def test_tox_get_python_executable():
def test_tox_get_python_executable(mocker):
class envconfig:
basepython = sys.executable
envname = "pyxx"
config = mocker.MagicMock()
config.return_value.option.return_value.discover = []

def get_exe(name):
envconfig.basepython = name
Expand Down Expand Up @@ -73,7 +75,7 @@ def assert_version_in_output(exe, version):


@pytest.mark.skipif("sys.platform == 'win32'", reason="symlink execution unreliable on Windows")
def test_find_alias_on_path(monkeypatch, tmp_path):
def test_find_alias_on_path(monkeypatch, tmp_path, mocker):
reporter.update_default_reporter(Verbosity.DEFAULT, Verbosity.DEBUG)
magic = tmp_path / "magic{}".format(os.path.splitext(sys.executable)[1])
os.symlink(sys.executable, str(magic))
Expand All @@ -85,6 +87,8 @@ def test_find_alias_on_path(monkeypatch, tmp_path):
class envconfig:
basepython = "magic"
envname = "pyxx"
config = mocker.MagicMock()
config.return_value.option.return_value.discover = []

detected = py.path.local.sysfind("magic")
assert detected
Expand All @@ -102,10 +106,12 @@ def test_run_and_get_interpreter_info():


class TestInterpreters:
def test_get_executable(self, interpreters):
def test_get_executable(self, interpreters, mocker):
class envconfig:
basepython = sys.executable
envname = "pyxx"
config = mocker.MagicMock()
config.return_value.option.return_value.discover = []

x = interpreters.get_executable(envconfig)
assert x == sys.executable
Expand All @@ -114,10 +120,12 @@ class envconfig:
assert info.executable == sys.executable
assert isinstance(info, InterpreterInfo)

def test_get_executable_no_exist(self, interpreters):
def test_get_executable_no_exist(self, interpreters, mocker):
class envconfig:
basepython = "1lkj23"
envname = "pyxx"
config = mocker.MagicMock()
config.return_value.option.return_value.discover = []

assert not interpreters.get_executable(envconfig)
info = interpreters.get_info(envconfig)
Expand Down Expand Up @@ -154,10 +162,12 @@ class envconfig:
info = interpreters.get_info(envconfig)
assert info.executable == str(magic)

def test_get_sitepackagesdir_error(self, interpreters):
def test_get_sitepackagesdir_error(self, interpreters, mocker):
class envconfig:
basepython = sys.executable
envname = "123"
config = mocker.MagicMock()
config.return_value.option.return_value.discover = []

info = interpreters.get_info(envconfig)
s = interpreters.get_sitepackagesdir(info, "")
Expand Down