python (3.11.7)
1 import collections
2 import os
3 import os.path
4 import subprocess
5 import sys
6 import sysconfig
7 import tempfile
8 from importlib import resources
9
10
11 __all__ = ["version", "bootstrap"]
12 _PACKAGE_NAMES = ('setuptools', 'pip')
13 _SETUPTOOLS_VERSION = "65.5.0"
14 _PIP_VERSION = "23.2.1"
15 _PROJECTS = [
16 ("setuptools", _SETUPTOOLS_VERSION, "py3"),
17 ("pip", _PIP_VERSION, "py3"),
18 ]
19
20 # Packages bundled in ensurepip._bundled have wheel_name set.
21 # Packages from WHEEL_PKG_DIR have wheel_path set.
22 _Package = collections.namedtuple('Package',
23 ('version', 'wheel_name', 'wheel_path'))
24
25 # Directory of system wheel packages. Some Linux distribution packaging
26 # policies recommend against bundling dependencies. For example, Fedora
27 # installs wheel packages in the /usr/share/python-wheels/ directory and don't
28 # install the ensurepip._bundled package.
29 _WHEEL_PKG_DIR = sysconfig.get_config_var('WHEEL_PKG_DIR')
30
31
32 def _find_packages(path):
33 packages = {}
34 try:
35 filenames = os.listdir(path)
36 except OSError:
37 # Ignore: path doesn't exist or permission error
38 filenames = ()
39 # Make the code deterministic if a directory contains multiple wheel files
40 # of the same package, but don't attempt to implement correct version
41 # comparison since this case should not happen.
42 filenames = sorted(filenames)
43 for filename in filenames:
44 # filename is like 'pip-21.2.4-py3-none-any.whl'
45 if not filename.endswith(".whl"):
46 continue
47 for name in _PACKAGE_NAMES:
48 prefix = name + '-'
49 if filename.startswith(prefix):
50 break
51 else:
52 continue
53
54 # Extract '21.2.4' from 'pip-21.2.4-py3-none-any.whl'
55 version = filename.removeprefix(prefix).partition('-')[0]
56 wheel_path = os.path.join(path, filename)
57 packages[name] = _Package(version, None, wheel_path)
58 return packages
59
60
61 def _get_packages():
62 global _PACKAGES, _WHEEL_PKG_DIR
63 if _PACKAGES is not None:
64 return _PACKAGES
65
66 packages = {}
67 for name, version, py_tag in _PROJECTS:
68 wheel_name = f"{name}-{version}-{py_tag}-none-any.whl"
69 packages[name] = _Package(version, wheel_name, None)
70 if _WHEEL_PKG_DIR:
71 dir_packages = _find_packages(_WHEEL_PKG_DIR)
72 # only used the wheel package directory if all packages are found there
73 if all(name in dir_packages for name in _PACKAGE_NAMES):
74 packages = dir_packages
75 _PACKAGES = packages
76 return packages
77 _PACKAGES = None
78
79
80 def _run_pip(args, additional_paths=None):
81 # Run the bootstrapping in a subprocess to avoid leaking any state that happens
82 # after pip has executed. Particularly, this avoids the case when pip holds onto
83 # the files in *additional_paths*, preventing us to remove them at the end of the
84 # invocation.
85 code = f"""
86 import runpy
87 import sys
88 sys.path = {additional_paths or []} + sys.path
89 sys.argv[1:] = {args}
90 runpy.run_module("pip", run_name="__main__", alter_sys=True)
91 """
92
93 cmd = [
94 sys.executable,
95 '-W',
96 'ignore::DeprecationWarning',
97 '-c',
98 code,
99 ]
100 if sys.flags.isolated:
101 # run code in isolated mode if currently running isolated
102 cmd.insert(1, '-I')
103 return subprocess.run(cmd, check=True).returncode
104
105
106 def version():
107 """
108 Returns a string specifying the bundled version of pip.
109 """
110 return _get_packages()['pip'].version
111
112
113 def _disable_pip_configuration_settings():
114 # We deliberately ignore all pip environment variables
115 # when invoking pip
116 # See http://bugs.python.org/issue19734 for details
117 keys_to_remove = [k for k in os.environ if k.startswith("PIP_")]
118 for k in keys_to_remove:
119 del os.environ[k]
120 # We also ignore the settings in the default pip configuration file
121 # See http://bugs.python.org/issue20053 for details
122 os.environ['PIP_CONFIG_FILE'] = os.devnull
123
124
125 def bootstrap(*, root=None, upgrade=False, user=False,
126 altinstall=False, default_pip=False,
127 verbosity=0):
128 """
129 Bootstrap pip into the current Python installation (or the given root
130 directory).
131
132 Note that calling this function will alter both sys.path and os.environ.
133 """
134 # Discard the return value
135 _bootstrap(root=root, upgrade=upgrade, user=user,
136 altinstall=altinstall, default_pip=default_pip,
137 verbosity=verbosity)
138
139
140 def _bootstrap(*, root=None, upgrade=False, user=False,
141 altinstall=False, default_pip=False,
142 verbosity=0):
143 """
144 Bootstrap pip into the current Python installation (or the given root
145 directory). Returns pip command status code.
146
147 Note that calling this function will alter both sys.path and os.environ.
148 """
149 if altinstall and default_pip:
150 raise ValueError("Cannot use altinstall and default_pip together")
151
152 sys.audit("ensurepip.bootstrap", root)
153
154 _disable_pip_configuration_settings()
155
156 # By default, installing pip and setuptools installs all of the
157 # following scripts (X.Y == running Python version):
158 #
159 # pip, pipX, pipX.Y, easy_install, easy_install-X.Y
160 #
161 # pip 1.5+ allows ensurepip to request that some of those be left out
162 if altinstall:
163 # omit pip, pipX and easy_install
164 os.environ["ENSUREPIP_OPTIONS"] = "altinstall"
165 elif not default_pip:
166 # omit pip and easy_install
167 os.environ["ENSUREPIP_OPTIONS"] = "install"
168
169 with tempfile.TemporaryDirectory() as tmpdir:
170 # Put our bundled wheels into a temporary directory and construct the
171 # additional paths that need added to sys.path
172 additional_paths = []
173 for name, package in _get_packages().items():
174 if package.wheel_name:
175 # Use bundled wheel package
176 wheel_name = package.wheel_name
177 wheel_path = resources.files("ensurepip") / "_bundled" / wheel_name
178 whl = wheel_path.read_bytes()
179 else:
180 # Use the wheel package directory
181 with open(package.wheel_path, "rb") as fp:
182 whl = fp.read()
183 wheel_name = os.path.basename(package.wheel_path)
184
185 filename = os.path.join(tmpdir, wheel_name)
186 with open(filename, "wb") as fp:
187 fp.write(whl)
188
189 additional_paths.append(filename)
190
191 # Construct the arguments to be passed to the pip command
192 args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir]
193 if root:
194 args += ["--root", root]
195 if upgrade:
196 args += ["--upgrade"]
197 if user:
198 args += ["--user"]
199 if verbosity:
200 args += ["-" + "v" * verbosity]
201
202 return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)
203
204 def _uninstall_helper(*, verbosity=0):
205 """Helper to support a clean default uninstall process on Windows
206
207 Note that calling this function may alter os.environ.
208 """
209 # Nothing to do if pip was never installed, or has been removed
210 try:
211 import pip
212 except ImportError:
213 return
214
215 # If the installed pip version doesn't match the available one,
216 # leave it alone
217 available_version = version()
218 if pip.__version__ != available_version:
219 print(f"ensurepip will only uninstall a matching version "
220 f"({pip.__version__!r} installed, "
221 f"{available_version!r} available)",
222 file=sys.stderr)
223 return
224
225 _disable_pip_configuration_settings()
226
227 # Construct the arguments to be passed to the pip command
228 args = ["uninstall", "-y", "--disable-pip-version-check"]
229 if verbosity:
230 args += ["-" + "v" * verbosity]
231
232 return _run_pip([*args, *reversed(_PACKAGE_NAMES)])
233
234
235 def _main(argv=None):
236 import argparse
237 parser = argparse.ArgumentParser(prog="python -m ensurepip")
238 parser.add_argument(
239 "--version",
240 action="version",
241 version="pip {}".format(version()),
242 help="Show the version of pip that is bundled with this Python.",
243 )
244 parser.add_argument(
245 "-v", "--verbose",
246 action="count",
247 default=0,
248 dest="verbosity",
249 help=("Give more output. Option is additive, and can be used up to 3 "
250 "times."),
251 )
252 parser.add_argument(
253 "-U", "--upgrade",
254 action="store_true",
255 default=False,
256 help="Upgrade pip and dependencies, even if already installed.",
257 )
258 parser.add_argument(
259 "--user",
260 action="store_true",
261 default=False,
262 help="Install using the user scheme.",
263 )
264 parser.add_argument(
265 "--root",
266 default=None,
267 help="Install everything relative to this alternate root directory.",
268 )
269 parser.add_argument(
270 "--altinstall",
271 action="store_true",
272 default=False,
273 help=("Make an alternate install, installing only the X.Y versioned "
274 "scripts (Default: pipX, pipX.Y, easy_install-X.Y)."),
275 )
276 parser.add_argument(
277 "--default-pip",
278 action="store_true",
279 default=False,
280 help=("Make a default pip install, installing the unqualified pip "
281 "and easy_install in addition to the versioned scripts."),
282 )
283
284 args = parser.parse_args(argv)
285
286 return _bootstrap(
287 root=args.root,
288 upgrade=args.upgrade,
289 user=args.user,
290 verbosity=args.verbosity,
291 altinstall=args.altinstall,
292 default_pip=args.default_pip,
293 )