Skip to content

Commit 6974665

Browse files
authored
Merge pull request #1219 from pypa/add-ci
* update detecting Windows Python installations - now correctly detects current user installation and not just those installed at the system level - for Python versions 3.5+ (which track 32-bit & 64-bit installations separately), recognize version tags X.Y, X.Y-32 & X.Y-64 where X.Y represents the 64-bit installation if available or 32-bit otherwise * touch up & improve `resolve_interpreter()` tests - tests no longer permanently modify the `virtualenv` module under test (`virtualenv.is_executable` changes were leaking from some tests) - tests now check that `get_installed_python()` results unrelated to the given version tag do not affect the result - simple patch function's return values now given in `@patch` decorators to make the test code more compact * test resolve_interpreter() with registered python installations * test `get_installed_pythons()` on and off Windows platform * add embed tox env * disable Python 3.4 * fix coverage reporter
2 parents af5711c + 707d592 commit 6974665

File tree

6 files changed

+247
-45
lines changed

6 files changed

+247
-45
lines changed

azure-pipelines.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ trigger:
2020
- .github/*
2121

2222
jobs:
23+
- template: azure-run-tox-env.yml
24+
parameters: {tox: embed, python: 3.7}
2325
- template: azure-run-tox-env.yml
2426
parameters: {tox: docs, python: 3.7}
2527
- template: azure-run-tox-env.yml

bin/rebuild-script.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def crc32(data):
1616

1717

1818
here = os.path.dirname(__file__)
19-
script = os.path.join(here, '..', 'virtualenv.py')
19+
script = os.path.join(here, '..', 'src', 'virtualenv.py')
2020

2121
gzip = codecs.lookup('zlib')
2222
b64 = codecs.lookup('base64')
@@ -28,6 +28,7 @@ def crc32(data):
2828

2929

3030
def rebuild(script_path):
31+
exit_code = 0
3132
with open(script_path, 'rb') as f:
3233
script_content = f.read()
3334
parts = []
@@ -52,6 +53,7 @@ def rebuild(script_path):
5253
print(' File up to date (crc: %08x)' % new_crc)
5354
parts += [match.group(0)]
5455
continue
56+
exit_code = 1
5557
# Else: content has changed
5658
crc = crc32(gzip.decode(b64.decode(data)[0])[0])
5759
print(' Content changed (crc: %08x -> %08x)' %
@@ -75,6 +77,7 @@ def rebuild(script_path):
7577
print('No changes in content')
7678
if match is None:
7779
print('No variables were matched/found')
80+
raise SystemExit(exit_code)
7881

7982
if __name__ == '__main__':
8083
rebuild(script)

src/virtualenv.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,33 +85,51 @@ def get_installed_pythons():
8585
import _winreg as winreg
8686

8787
def get_installed_pythons():
88-
try:
89-
python_core = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE,
90-
"Software\\Python\\PythonCore")
91-
except WindowsError:
92-
# No registered Python installations
93-
return {}
94-
i = 0
95-
versions = []
96-
while True:
97-
try:
98-
versions.append(winreg.EnumKey(python_core, i))
99-
i = i + 1
100-
except WindowsError:
101-
break
10288
exes = dict()
103-
for ver in versions:
89+
# If both system and current user installations are found for a
90+
# particular Python version, the current user one is used
91+
for key in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
10492
try:
105-
path = winreg.QueryValue(python_core, "%s\\InstallPath" % ver)
93+
python_core = winreg.CreateKey(key,
94+
"Software\\Python\\PythonCore")
10695
except WindowsError:
96+
# No registered Python installations
10797
continue
108-
exes[ver] = join(path, "python.exe")
109-
110-
winreg.CloseKey(python_core)
98+
i = 0
99+
while True:
100+
try:
101+
version = winreg.EnumKey(python_core, i)
102+
i += 1
103+
try:
104+
path = winreg.QueryValue(python_core,
105+
"%s\\InstallPath" % version)
106+
except WindowsError:
107+
continue
108+
exes[version] = join(path, "python.exe")
109+
except WindowsError:
110+
break
111+
winreg.CloseKey(python_core)
112+
113+
# For versions that track separate 32-bit (`X.Y-32`) & 64-bit (`X-Y`)
114+
# installation registrations, add a `X.Y-64` version tag and make the
115+
# extensionless `X.Y` version tag represent the 64-bit installation if
116+
# available or 32-bit if it is not
117+
updated = {}
118+
for ver in exes:
119+
if ver < '3.5':
120+
continue
121+
if ver.endswith('-32'):
122+
base_ver = ver[:-3]
123+
if base_ver not in exes:
124+
updated[base_ver] = exes[ver]
125+
else:
126+
updated[ver + '-64'] = exes[ver]
127+
exes.update(updated)
111128

112129
# Add the major versions
113130
# Sort the keys, then repeatedly update the major version entry
114-
# Last executable (i.e., highest version) wins with this approach
131+
# Last executable (i.e., highest version) wins with this approach,
132+
# 64-bit over 32-bit if both are found
115133
for ver in sorted(exes):
116134
exes[ver[0]] = exes[ver]
117135

tests/test_cmdline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_commandline_explicit_interp(tmpdir):
2727
# registry layout is not well documented, and it's not clear that the feature
2828
# is sufficiently widely used to be worth fixing.
2929
# See https://github.com/pypa/virtualenv/issues/864
30-
@pytest.mark.skipif("sys.platform == 'win32' and sys.version_info[:2] >= (3,5)")
30+
@pytest.mark.skipif("sys.platform == 'win32' and sys.version_info[:1] >= (3,)")
3131
def test_commandline_abbrev_interp(tmpdir):
3232
"""Specifying abbreviated forms of the Python interpreter should work"""
3333
if sys.platform == 'win32':

tests/test_virtualenv.py

Lines changed: 187 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,224 @@
77
import pytest
88
import platform # noqa
99

10-
from mock import patch, Mock
10+
from mock import call, Mock, NonCallableMock, patch
1111

1212

1313
def test_version():
1414
"""Should have a version string"""
1515
assert virtualenv.virtualenv_version, "Should have version"
1616

1717

18-
@patch('os.path.exists')
19-
def test_resolve_interpreter_with_absolute_path(mock_exists):
18+
class TestGetInstalledPythons:
19+
key_local_machine = 'key-local-machine'
20+
key_current_user = 'key-current-user'
21+
22+
@classmethod
23+
def mock_virtualenv_winreg(cls, monkeypatch, data):
24+
def enum_key(key, index):
25+
try:
26+
return data.get(key, [])[index]
27+
except IndexError:
28+
raise WindowsError
29+
30+
def query_value(key, path):
31+
installed_version_tags = data.get(key, [])
32+
suffix = '\\InstallPath'
33+
if path.endswith(suffix):
34+
version_tag = path[:-len(suffix)]
35+
if version_tag in installed_version_tags:
36+
return '{}-{}-path'.format(key, version_tag)
37+
raise WindowsError
38+
39+
mock_winreg = NonCallableMock(spec_set=[
40+
'HKEY_LOCAL_MACHINE',
41+
'HKEY_CURRENT_USER',
42+
'CreateKey',
43+
'EnumKey',
44+
'QueryValue',
45+
'CloseKey'])
46+
mock_winreg.HKEY_LOCAL_MACHINE = 'HKEY_LOCAL_MACHINE'
47+
mock_winreg.HKEY_CURRENT_USER = 'HKEY_CURRENT_USER'
48+
mock_winreg.CreateKey.side_effect = [cls.key_local_machine,
49+
cls.key_current_user]
50+
mock_winreg.EnumKey.side_effect = enum_key
51+
mock_winreg.QueryValue.side_effect = query_value
52+
mock_winreg.CloseKey.return_value = None
53+
monkeypatch.setattr(virtualenv, 'winreg', mock_winreg)
54+
return mock_winreg
55+
56+
@pytest.mark.skipif(sys.platform == 'win32',
57+
reason='non-windows specific test')
58+
def test_on_non_windows(self, monkeypatch):
59+
assert not virtualenv.is_win
60+
assert not hasattr(virtualenv, 'winreg')
61+
assert virtualenv.get_installed_pythons() == {}
62+
63+
@pytest.mark.skipif(sys.platform != 'win32',
64+
reason='non-windows specific test')
65+
def test_on_windows(self, monkeypatch):
66+
assert virtualenv.is_win
67+
mock_winreg = self.mock_virtualenv_winreg(monkeypatch, {
68+
self.key_local_machine: (
69+
'2.4',
70+
'2.7',
71+
'3.2',
72+
'3.4',
73+
'3.5', # 64-bit only
74+
'3.6-32', # 32-bit only
75+
'3.7', '3.7-32', # both 32 & 64-bit with a 64-bit user install
76+
'3.8'), # 64-bit with a 32-bit user install
77+
self.key_current_user: (
78+
'2.5',
79+
'2.7',
80+
'3.7',
81+
'3.8-32')})
82+
monkeypatch.setattr(virtualenv, 'join', '{}\\{}'.format)
83+
84+
installed_pythons = virtualenv.get_installed_pythons()
85+
86+
assert installed_pythons == {
87+
'2': self.key_current_user + '-2.7-path\\python.exe',
88+
'2.4': self.key_local_machine + '-2.4-path\\python.exe',
89+
'2.5': self.key_current_user + '-2.5-path\\python.exe',
90+
'2.7': self.key_current_user + '-2.7-path\\python.exe',
91+
'3': self.key_local_machine + '-3.8-path\\python.exe',
92+
'3.2': self.key_local_machine + '-3.2-path\\python.exe',
93+
'3.4': self.key_local_machine + '-3.4-path\\python.exe',
94+
'3.5': self.key_local_machine + '-3.5-path\\python.exe',
95+
'3.5-64': self.key_local_machine + '-3.5-path\\python.exe',
96+
'3.6': self.key_local_machine + '-3.6-32-path\\python.exe',
97+
'3.6-32': self.key_local_machine + '-3.6-32-path\\python.exe',
98+
'3.7': self.key_current_user + '-3.7-path\\python.exe',
99+
'3.7-32': self.key_local_machine + '-3.7-32-path\\python.exe',
100+
'3.7-64': self.key_current_user + '-3.7-path\\python.exe',
101+
'3.8': self.key_local_machine + '-3.8-path\\python.exe',
102+
'3.8-32': self.key_current_user + '-3.8-32-path\\python.exe',
103+
'3.8-64': self.key_local_machine + '-3.8-path\\python.exe'}
104+
assert mock_winreg.mock_calls == [
105+
call.CreateKey(mock_winreg.HKEY_LOCAL_MACHINE,
106+
'Software\\Python\\PythonCore'),
107+
call.EnumKey(self.key_local_machine, 0),
108+
call.QueryValue(self.key_local_machine, '2.4\\InstallPath'),
109+
call.EnumKey(self.key_local_machine, 1),
110+
call.QueryValue(self.key_local_machine, '2.7\\InstallPath'),
111+
call.EnumKey(self.key_local_machine, 2),
112+
call.QueryValue(self.key_local_machine, '3.2\\InstallPath'),
113+
call.EnumKey(self.key_local_machine, 3),
114+
call.QueryValue(self.key_local_machine, '3.4\\InstallPath'),
115+
call.EnumKey(self.key_local_machine, 4),
116+
call.QueryValue(self.key_local_machine, '3.5\\InstallPath'),
117+
call.EnumKey(self.key_local_machine, 5),
118+
call.QueryValue(self.key_local_machine, '3.6-32\\InstallPath'),
119+
call.EnumKey(self.key_local_machine, 6),
120+
call.QueryValue(self.key_local_machine, '3.7\\InstallPath'),
121+
call.EnumKey(self.key_local_machine, 7),
122+
call.QueryValue(self.key_local_machine, '3.7-32\\InstallPath'),
123+
call.EnumKey(self.key_local_machine, 8),
124+
call.QueryValue(self.key_local_machine, '3.8\\InstallPath'),
125+
call.EnumKey(self.key_local_machine, 9),
126+
call.CloseKey(self.key_local_machine),
127+
call.CreateKey(mock_winreg.HKEY_CURRENT_USER,
128+
'Software\\Python\\PythonCore'),
129+
call.EnumKey(self.key_current_user, 0),
130+
call.QueryValue(self.key_current_user, '2.5\\InstallPath'),
131+
call.EnumKey(self.key_current_user, 1),
132+
call.QueryValue(self.key_current_user, '2.7\\InstallPath'),
133+
call.EnumKey(self.key_current_user, 2),
134+
call.QueryValue(self.key_current_user, '3.7\\InstallPath'),
135+
call.EnumKey(self.key_current_user, 3),
136+
call.QueryValue(self.key_current_user, '3.8-32\\InstallPath'),
137+
call.EnumKey(self.key_current_user, 4),
138+
call.CloseKey(self.key_current_user)]
139+
140+
@pytest.mark.skipif(sys.platform != 'win32',
141+
reason='windows specific test')
142+
def test_on_windows_with_no_installations(self, monkeypatch):
143+
assert virtualenv.is_win
144+
mock_winreg = self.mock_virtualenv_winreg(monkeypatch, {})
145+
146+
installed_pythons = virtualenv.get_installed_pythons()
147+
148+
assert installed_pythons == {}
149+
assert mock_winreg.mock_calls == [
150+
call.CreateKey(mock_winreg.HKEY_LOCAL_MACHINE,
151+
'Software\\Python\\PythonCore'),
152+
call.EnumKey(self.key_local_machine, 0),
153+
call.CloseKey(self.key_local_machine),
154+
call.CreateKey(mock_winreg.HKEY_CURRENT_USER,
155+
'Software\\Python\\PythonCore'),
156+
call.EnumKey(self.key_current_user, 0),
157+
call.CloseKey(self.key_current_user)]
158+
159+
160+
@patch('distutils.spawn.find_executable')
161+
@patch('virtualenv.is_executable', return_value=True)
162+
@patch('virtualenv.get_installed_pythons')
163+
@patch('os.path.exists', return_value=True)
164+
@patch('os.path.abspath')
165+
def test_resolve_interpreter_with_installed_python(mock_abspath, mock_exists,
166+
mock_get_installed_pythons, mock_is_executable, mock_find_executable):
167+
test_tag = 'foo'
168+
test_path = '/path/to/foo/python.exe'
169+
test_abs_path = 'some-abs-path'
170+
test_found_path = 'some-found-path'
171+
mock_get_installed_pythons.return_value = {
172+
test_tag: test_path,
173+
test_tag + '2': test_path + '2'}
174+
mock_abspath.return_value = test_abs_path
175+
mock_find_executable.return_value = test_found_path
176+
177+
exe = virtualenv.resolve_interpreter('foo')
178+
179+
assert exe == test_found_path, \
180+
"installed python should be accessible by key"
181+
182+
mock_get_installed_pythons.assert_called_once_with()
183+
mock_abspath.assert_called_once_with(test_path)
184+
mock_find_executable.assert_called_once_with(test_path)
185+
mock_exists.assert_called_once_with(test_found_path)
186+
mock_is_executable.assert_called_once_with(test_found_path)
187+
188+
189+
@patch('virtualenv.is_executable', return_value=True)
190+
@patch('virtualenv.get_installed_pythons', return_value={'foo': 'bar'})
191+
@patch('os.path.exists', return_value=True)
192+
def test_resolve_interpreter_with_absolute_path(
193+
mock_exists, mock_get_installed_pythons, mock_is_executable):
20194
"""Should return absolute path if given and exists"""
21-
mock_exists.return_value = True
22-
virtualenv.is_executable = Mock(return_value=True)
23195
test_abs_path = os.path.abspath("/usr/bin/python53")
24196

25197
exe = virtualenv.resolve_interpreter(test_abs_path)
26198

27199
assert exe == test_abs_path, "Absolute path should return as is"
28200

29201
mock_exists.assert_called_with(test_abs_path)
30-
virtualenv.is_executable.assert_called_with(test_abs_path)
202+
mock_is_executable.assert_called_with(test_abs_path)
31203

32204

33-
@patch('os.path.exists')
34-
def test_resolve_interpreter_with_nonexistent_interpreter(mock_exists):
205+
@patch('virtualenv.get_installed_pythons', return_value={'foo': 'bar'})
206+
@patch('os.path.exists', return_value=False)
207+
def test_resolve_interpreter_with_nonexistent_interpreter(
208+
mock_exists, mock_get_installed_pythons):
35209
"""Should SystemExit with an nonexistent python interpreter path"""
36-
mock_exists.return_value = False
37-
38210
with pytest.raises(SystemExit):
39211
virtualenv.resolve_interpreter("/usr/bin/python53")
40212

41213
mock_exists.assert_called_with("/usr/bin/python53")
42214

43215

44-
@patch('os.path.exists')
45-
def test_resolve_interpreter_with_invalid_interpreter(mock_exists):
216+
@patch('virtualenv.is_executable', return_value=False)
217+
@patch('os.path.exists', return_value=True)
218+
def test_resolve_interpreter_with_invalid_interpreter(mock_exists,
219+
mock_is_executable):
46220
"""Should exit when with absolute path if not exists"""
47-
mock_exists.return_value = True
48-
virtualenv.is_executable = Mock(return_value=False)
49221
invalid = os.path.abspath("/usr/bin/pyt_hon53")
50222

51223
with pytest.raises(SystemExit):
52224
virtualenv.resolve_interpreter(invalid)
53225

54226
mock_exists.assert_called_with(invalid)
55-
virtualenv.is_executable.assert_called_with(invalid)
227+
mock_is_executable.assert_called_with(invalid)
56228

57229

58230
def test_activate_after_future_statements():

0 commit comments

Comments
 (0)