Skip to content

Commit f2ea8e2

Browse files
authored
Merge pull request RustPython#3794 from youknowone/ensure-pip
Add ensurepip
2 parents 7917d12 + e3be6e5 commit f2ea8e2

File tree

7 files changed

+687
-0
lines changed

7 files changed

+687
-0
lines changed

Lib/ensurepip/__init__.py

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

Lib/ensurepip/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ensurepip
2+
import sys
3+
4+
if __name__ == "__main__":
5+
sys.exit(ensurepip._main())

Lib/ensurepip/_bundled/__init__.py

Whitespace-only changes.
2.03 MB
Binary file not shown.
798 KB
Binary file not shown.

Lib/ensurepip/_uninstall.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Basic pip uninstallation support, helper for the Windows uninstaller"""
2+
3+
import argparse
4+
import ensurepip
5+
import sys
6+
7+
8+
def _main(argv=None):
9+
parser = argparse.ArgumentParser(prog="python -m ensurepip._uninstall")
10+
parser.add_argument(
11+
"--version",
12+
action="version",
13+
version="pip {}".format(ensurepip.version()),
14+
help="Show the version of pip this will attempt to uninstall.",
15+
)
16+
parser.add_argument(
17+
"-v", "--verbose",
18+
action="count",
19+
default=0,
20+
dest="verbosity",
21+
help=("Give more output. Option is additive, and can be used up to 3 "
22+
"times."),
23+
)
24+
25+
args = parser.parse_args(argv)
26+
27+
return ensurepip._uninstall_helper(verbosity=args.verbosity)
28+
29+
30+
if __name__ == "__main__":
31+
sys.exit(_main())

0 commit comments

Comments
 (0)