Skip to content

Commit e2dcb69

Browse files
committed
🎨✨ Implement ensurepip artifacts downloader
1 parent 396466b commit e2dcb69

File tree

1 file changed

+137
-23
lines changed

1 file changed

+137
-23
lines changed

‎Lib/ensurepip/__init__.py

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,112 @@
11
import os
22
import os.path
3-
import pkgutil
43
import sys
54
import tempfile
65

76

87
__all__ = ["version", "bootstrap"]
98

109

11-
_SETUPTOOLS_VERSION = "40.8.0"
10+
_PROJECT_URLS = (
11+
'https://files.pythonhosted.org/packages/c8/b0/'
12+
'cc6b7ba28d5fb790cf0d5946df849233e32b8872b6baca10c9e002ff5b41/'
13+
'setuptools-41.0.0-py2.py3-none-any.whl#'
14+
'sha256=e67486071cd5cdeba783bd0b64f5f30784ff855b35071c8670551fd7fc52d4a1',
1215

13-
_PIP_VERSION = "19.0.3"
16+
'https://files.pythonhosted.org/packages/d8/f3/'
17+
'413bab4ff08e1fc4828dfc59996d721917df8e8583ea85385d51125dceff/'
18+
'pip-19.0.3-py2.py3-none-any.whl#'
19+
'sha256=bd812612bbd8ba84159d9ddc0266b7fbce712fc9bc98c82dee5750546ec8ec64',
20+
)
1421

15-
_PROJECTS = [
16-
("setuptools", _SETUPTOOLS_VERSION),
17-
("pip", _PIP_VERSION),
18-
]
22+
23+
def _extract_sha256_from_url_fragment(*, url):
24+
"""Extract SHA-256 hash from the given URL fragment part."""
25+
import urllib.parse
26+
url_fragment = urllib.parse.urlsplit(url).fragment.strip()
27+
return next(
28+
iter(urllib.parse.parse_qs(url_fragment).get("sha256")),
29+
None,
30+
)
31+
32+
33+
def _is_content_sha256_valid(*, content, sha256):
34+
import hashlib
35+
return (
36+
sha256 is None or
37+
sha256 == hashlib.sha256(content).hexdigest()
38+
)
39+
40+
41+
def _download(*, url, sha256):
42+
"""Retrieve the given URL contents and verify hash if needed.
43+
44+
If hash in the URL fragment doesn't match downloaded one, raise a
45+
ValueError.
46+
47+
Return the URL contents as a memoryview object on success.
48+
"""
49+
import urllib.request
50+
51+
with urllib.request.urlopen(url) as f:
52+
resource_content = memoryview(f.read())
53+
54+
if not _is_content_sha256_valid(content=resource_content, sha256=sha256):
55+
raise ValueError(f"The payload's hash is invalid for ``{url}``.")
56+
57+
return resource_content
58+
59+
60+
def _get_name_and_url(url):
61+
import urllib.parse
62+
url_path = urllib.parse.urlsplit(url).path
63+
_path_dir, _sep, file_name = url_path.rpartition('/')
64+
sha256 = _extract_sha256_from_url_fragment(url=url)
65+
return file_name, url, sha256
66+
67+
68+
def _get_name_and_version(url):
69+
return tuple(_get_name_and_url(url)[0].split('-')[:2])
70+
71+
72+
def _ensure_wheels_are_downloaded(*, verbosity=0):
73+
"""Download wheels into bundle if they are not there yet."""
74+
import importlib.resources
75+
76+
with importlib.resources.path('ensurepip', '_bundled') as bundled_dir:
77+
wheels = map(_get_name_and_url, _PROJECT_URLS)
78+
for wheel_file_name, project_url, wheel_sha256 in wheels:
79+
whl_file_path = bundled_dir / wheel_file_name
80+
try:
81+
if _is_content_sha256_valid(
82+
content=whl_file_path.read_bytes(),
83+
sha256=wheel_sha256,
84+
):
85+
if verbosity:
86+
print(
87+
f'A valid `{wheel_file_name}` is already '
88+
'present in cache. Skipping download.',
89+
file=sys.stderr,
90+
)
91+
continue
92+
except FileNotFoundError:
93+
pass
94+
95+
if verbosity:
96+
print(
97+
f'Downloading `{wheel_file_name}`...',
98+
file=sys.stderr,
99+
)
100+
downloaded_whl_contents = _download(
101+
url=project_url,
102+
sha256=wheel_sha256,
103+
)
104+
if verbosity:
105+
print(
106+
f'Saving `{wheel_file_name}` to disk...',
107+
file=sys.stderr,
108+
)
109+
whl_file_path.write_bytes(downloaded_whl_contents)
19110

20111

21112
def _run_pip(args, additional_paths=None):
@@ -32,7 +123,16 @@ def version():
32123
"""
33124
Returns a string specifying the bundled version of pip.
34125
"""
35-
return _PIP_VERSION
126+
try:
127+
return next(
128+
v for n, v in (
129+
_get_name_and_version(pu) for pu in _PROJECT_URLS
130+
)
131+
if n == 'pip'
132+
)
133+
except StopIteration:
134+
raise RuntimeError('Failed to get bundled Pip version')
135+
36136

37137
def _disable_pip_configuration_settings():
38138
# We deliberately ignore all pip environment variables
@@ -70,6 +170,10 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
70170
71171
Note that calling this function will alter both sys.path and os.environ.
72172
"""
173+
import importlib.resources
174+
import pathlib
175+
import shutil
176+
73177
if altinstall and default_pip:
74178
raise ValueError("Cannot use altinstall and default_pip together")
75179

@@ -88,20 +192,25 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
88192
# omit pip and easy_install
89193
os.environ["ENSUREPIP_OPTIONS"] = "install"
90194

195+
# Ensure that the downloaded wheels are there
196+
_ensure_wheels_are_downloaded(verbosity=verbosity)
197+
91198
with tempfile.TemporaryDirectory() as tmpdir:
92199
# Put our bundled wheels into a temporary directory and construct the
93200
# additional paths that need added to sys.path
201+
tmpdir_path = pathlib.Path(tmpdir)
94202
additional_paths = []
95-
for project, version in _PROJECTS:
96-
wheel_name = "{}-{}-py2.py3-none-any.whl".format(project, version)
97-
whl = pkgutil.get_data(
98-
"ensurepip",
99-
"_bundled/{}".format(wheel_name),
100-
)
101-
with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
102-
fp.write(whl)
203+
wheels = map(_get_name_and_url, _PROJECT_URLS)
204+
for wheel_file_name, project_url, wheel_sha256 in wheels:
205+
tmp_wheel_path = tmpdir_path / wheel_file_name
206+
207+
with importlib.resources.path(
208+
'ensurepip', '_bundled',
209+
) as bundled_dir:
210+
bundled_wheel = bundled_dir / wheel_file_name
211+
shutil.copy2(bundled_wheel, tmp_wheel_path)
103212

104-
additional_paths.append(os.path.join(tmpdir, wheel_name))
213+
additional_paths.append(str(tmp_wheel_path))
105214

106215
# Construct the arguments to be passed to the pip command
107216
args = ["install", "--no-index", "--find-links", tmpdir]
@@ -114,7 +223,8 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
114223
if verbosity:
115224
args += ["-" + "v" * verbosity]
116225

117-
return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
226+
wheels_specs = map(_get_name_and_version, _PROJECT_URLS)
227+
return _run_pip(args + [p[0] for p in wheels_specs], additional_paths)
118228

119229
def _uninstall_helper(*, verbosity=0):
120230
"""Helper to support a clean default uninstall process on Windows
@@ -127,11 +237,14 @@ def _uninstall_helper(*, verbosity=0):
127237
except ImportError:
128238
return
129239

240+
pip_version = version()
130241
# If the pip version doesn't match the bundled one, leave it alone
131-
if pip.__version__ != _PIP_VERSION:
132-
msg = ("ensurepip will only uninstall a matching version "
133-
"({!r} installed, {!r} bundled)")
134-
print(msg.format(pip.__version__, _PIP_VERSION), file=sys.stderr)
242+
if pip.__version__ != pip_version:
243+
err_msg = (
244+
"ensurepip will only uninstall a matching version "
245+
f"({pip.__version__!r} installed, {pip_version!r} bundled)"
246+
)
247+
print(err_msg, file=sys.stderr)
135248
return
136249

137250
_disable_pip_configuration_settings()
@@ -141,7 +254,8 @@ def _uninstall_helper(*, verbosity=0):
141254
if verbosity:
142255
args += ["-" + "v" * verbosity]
143256

144-
return _run_pip(args + [p[0] for p in reversed(_PROJECTS)])
257+
wheels_specs = map(_get_name_and_version, _PROJECT_URLS)
258+
return _run_pip(args + [p[0] for p in reversed(tuple(wheels_specs))])
145259

146260

147261
def _main(argv=None):

0 commit comments

Comments
 (0)