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

Locate the Python interpreter via the py.exe launcher #53

Merged
merged 6 commits into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from 5 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
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonparrott Should this be a None-check?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. If you want you can send a follow-up PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done #59

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