Skip to content

Commit

Permalink
Locate the Python interpreter via the py.exe launcher (wntrblm#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
pfmoore authored and Jon Wayne Parrott committed Sep 20, 2017
1 parent 832b7cf commit 95863f7
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 22 deletions.
36 changes: 26 additions & 10 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ def run(self, args, in_venv=True):
path=self.bin if in_venv else None).run()


def locate_via_py(version):
"""Find the Python executable using the Windows launcher.
This is based on :pep:397 which details that executing
``py.exe -{version}`` should execute python with the requested
version. We then make the python process print out its full
executable path which we use as the location for the version-
specific Python interpreter.
"""
script = "import sys; print(sys.executable)"
py_exe = py.path.local.sysfind('py')
if py_exe:
try:
return py_exe.sysexec('-' + version, '-c', script).strip()
except py.process.cmdexec.Error:
return None


class VirtualEnv(ProcessEnv):
"""Virtualenv management class."""

Expand Down Expand Up @@ -102,17 +120,15 @@ def _resolved_interpreter(self):
return self.interpreter

# If this is a standard Unix "pythonX.Y" name, it should be found
# in a standard location in Windows.
match = re.match(r'^python(?P<maj>\d)\.(?P<min>\d)$', self.interpreter)
# in a standard location in Windows, and if not, the py.exe launcher
# should be able to find it from the information in the registry.
match = re.match(r'^python(?P<ver>\d\.\d)$', self.interpreter)
if match:
version = match.groupdict()
potential_paths = (
r'c:\python{maj}{min}\python.exe'.format(**version),
r'c:\python{maj}{min}-x64\python.exe'.format(**version),
)
for path in potential_paths:
if py.path.local(path).check():
return str(path)
version = match.group('ver')
# Ask the Python launcher to find the interpreter.
path_from_launcher = locate_via_py(version)
if path_from_launcher:
return path_from_launcher

# If we got this far, then we were unable to resolve the interpreter
# to an actual executable; raise an exception.
Expand Down
49 changes: 37 additions & 12 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,28 +226,53 @@ def test__resolved_interpreter_windows_full_path(make_one):


@mock.patch.object(platform, 'system')
@mock.patch.object(py._path.local.LocalPath, 'check')
@mock.patch.object(py.path.local, 'sysfind')
def test__resolved_interpreter_windows_stloc(sysfind, check, system, make_one):
# Establish that if we get a standard pythonX.Y path, we map it to
# standard locations on Windows.
def test__resolved_interpreter_windows_pyexe(sysfind, system, make_one):
# Establish that if we get a standard pythonX.Y path, we look it
# up via the py launcher on Windows.
venv, _ = make_one(interpreter='python3.6')

# Trick the system into thinking we are on Windows.
system.return_value = 'Windows'

# Trick the system into thinking that it cannot find python3.6
# (it likely will on Unix).
sysfind.return_value = False

# Trick the system into thinking it _can_ find it in the Windows
# standard location.
check.return_value = True
# (it likely will on Unix). Also, when the system looks for the
# py launcher, give it a dummy that returns our test value when
# run.
attrs = {'sysexec.return_value': r'c:\python36\python.exe'}
mock_py = mock.Mock()
mock_py.configure_mock(**attrs)
sysfind.side_effect = lambda arg: mock_py if arg == 'py' else False

# Okay now run the test.
assert venv._resolved_interpreter == r'c:\python36\python.exe'
check.assert_called_once_with()
sysfind.assert_called_once_with('python3.6')
sysfind.assert_any_call('python3.6')
sysfind.assert_any_call('py')
system.assert_called()


@mock.patch.object(platform, 'system')
@mock.patch.object(py.path.local, 'sysfind')
def test__resolved_interpreter_windows_pyexe_fails(sysfind, system, make_one):
# Establish that if the py launcher fails, we give the right error.
venv, _ = make_one(interpreter='python3.6')

# Trick the system into thinking we are on Windows.
system.return_value = 'Windows'

# Trick the system into thinking that it cannot find python3.6
# (it likely will on Unix). Also, when the system looks for the
# py launcher, give it a dummy that fails.
attrs = {'sysexec.side_effect': py.process.cmdexec.Error(1, 1, '', '', '')}
mock_py = mock.Mock()
mock_py.configure_mock(**attrs)
sysfind.side_effect = lambda arg: mock_py if arg == 'py' else False

# Okay now run the test.
with pytest.raises(RuntimeError):
venv._resolved_interpreter
sysfind.assert_any_call('python3.6')
sysfind.assert_any_call('py')
system.assert_called()


Expand Down

0 comments on commit 95863f7

Please sign in to comment.