From 95863f73f0716130724109c0134c31dd8f89a073 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Wed, 20 Sep 2017 17:02:27 +0100
Subject: [PATCH] Locate the Python interpreter via the py.exe launcher (#53)
---
nox/virtualenv.py | 36 +++++++++++++++++++++--------
tests/test_virtualenv.py | 49 ++++++++++++++++++++++++++++++----------
2 files changed, 63 insertions(+), 22 deletions(-)
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index bf930f7b..0296da08 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -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."""
@@ -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\d)\.(?P\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\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.
diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py
index 09c0f9d9..78b6d847 100644
--- a/tests/test_virtualenv.py
+++ b/tests/test_virtualenv.py
@@ -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()