11import os
22import os .path
3- import pkgutil
43import sys
54import 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
21112def _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
37137def _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
119229def _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
147261def _main (argv = None ):
0 commit comments