Skip to content

Commit ed28573

Browse files
vstinnermgorny
authored andcommitted
bpo-42856: Add --with-wheel-pkg-dir=PATH configure option (pythonGH-24210)
Add --with-wheel-pkg-dir=PATH option to the ./configure script. If specified, the ensurepip module looks for setuptools and pip wheel packages in this directory: if both are present, these wheel packages are used instead of ensurepip bundled wheel packages. Some Linux distribution packaging policies recommend against bundling dependencies. For example, Fedora installs wheel packages in the /usr/share/python-wheels/ directory and don't install the ensurepip._bundled package. ensurepip: Remove unused runpy import. backported to 3.8 by Michał Górny
1 parent a830092 commit ed28573

6 files changed

Lines changed: 198 additions & 32 deletions

File tree

Doc/library/ensurepip.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The simplest possible invocation is::
4848

4949
This invocation will install ``pip`` if it is not already installed,
5050
but otherwise does nothing. To ensure the installed version of ``pip``
51-
is at least as recent as the one bundled with ``ensurepip``, pass the
51+
is at least as recent as the one available in ``ensurepip``, pass the
5252
``--upgrade`` option::
5353

5454
python -m ensurepip --upgrade
@@ -86,7 +86,7 @@ Module API
8686

8787
.. function:: version()
8888

89-
Returns a string specifying the bundled version of pip that will be
89+
Returns a string specifying the available version of pip that will be
9090
installed when bootstrapping an environment.
9191

9292
.. function:: bootstrap(root=None, upgrade=False, user=False, \
@@ -100,7 +100,7 @@ Module API
100100
for the current environment.
101101

102102
*upgrade* indicates whether or not to upgrade an existing installation
103-
of an earlier version of ``pip`` to the bundled version.
103+
of an earlier version of ``pip`` to the available version.
104104

105105
*user* indicates whether to use the user scheme rather than installing
106106
globally.

Lib/ensurepip/__init__.py

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1+
import collections
12
import os
23
import os.path
4+
import subprocess
35
import sys
4-
import runpy
6+
import sysconfig
57
import tempfile
6-
import subprocess
78
from importlib import resources
89

9-
from . import _bundled
10-
1110

1211

1312
__all__ = ["version", "bootstrap"]
@@ -19,6 +18,65 @@
1918
("pip", _PIP_VERSION, "py3"),
2019
]
2120

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-20.2.3-py2.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 '20.2.2' from 'pip-20.2.2-py2.py3-none-any.whl'
56+
version = filename[len(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+
2280

2381
def _run_pip(args, additional_paths=None):
2482
# Run the bootstraping in a subprocess to avoid leaking any state that happens
@@ -44,7 +102,8 @@ def version():
44102
"""
45103
Returns a string specifying the bundled version of pip.
46104
"""
47-
return _PIP_VERSION
105+
return _get_packages()['pip'].version
106+
48107

49108
def _disable_pip_configuration_settings():
50109
# We deliberately ignore all pip environment variables
@@ -106,16 +165,23 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
106165
# Put our bundled wheels into a temporary directory and construct the
107166
# additional paths that need added to sys.path
108167
additional_paths = []
109-
for project, version, py_tag in _PROJECTS:
110-
wheel_name = "{}-{}-{}-none-any.whl".format(project, version, py_tag)
111-
whl = resources.read_binary(
112-
_bundled,
113-
wheel_name,
114-
)
115-
with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
168+
for name, package in _get_packages().items():
169+
if package.wheel_name:
170+
# Use bundled wheel package
171+
from ensurepip import _bundled
172+
wheel_name = package.wheel_name
173+
whl = resources.read_binary(_bundled, wheel_name)
174+
else:
175+
# Use the wheel package directory
176+
with open(package.wheel_path, "rb") as fp:
177+
whl = fp.read()
178+
wheel_name = os.path.basename(package.wheel_path)
179+
180+
filename = os.path.join(tmpdir, wheel_name)
181+
with open(filename, "wb") as fp:
116182
fp.write(whl)
117183

118-
additional_paths.append(os.path.join(tmpdir, wheel_name))
184+
additional_paths.append(filename)
119185

120186
# Construct the arguments to be passed to the pip command
121187
args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir]
@@ -128,7 +194,7 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
128194
if verbosity:
129195
args += ["-" + "v" * verbosity]
130196

131-
return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
197+
return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)
132198

133199
def _uninstall_helper(*, verbosity=0):
134200
"""Helper to support a clean default uninstall process on Windows
@@ -141,11 +207,14 @@ def _uninstall_helper(*, verbosity=0):
141207
except ImportError:
142208
return
143209

144-
# If the pip version doesn't match the bundled one, leave it alone
145-
if pip.__version__ != _PIP_VERSION:
146-
msg = ("ensurepip will only uninstall a matching version "
147-
"({!r} installed, {!r} bundled)")
148-
print(msg.format(pip.__version__, _PIP_VERSION), file=sys.stderr)
210+
# If the installed pip version doesn't match the available one,
211+
# leave it alone
212+
available_version = version()
213+
if pip.__version__ != available_version:
214+
print(f"ensurepip will only uninstall a matching version "
215+
f"({pip.__version__!r} installed, "
216+
f"{available_version!r} available)",
217+
file=sys.stderr)
149218
return
150219

151220
_disable_pip_configuration_settings()
@@ -155,7 +224,7 @@ def _uninstall_helper(*, verbosity=0):
155224
if verbosity:
156225
args += ["-" + "v" * verbosity]
157226

158-
return _run_pip(args + [p[0] for p in reversed(_PROJECTS)])
227+
return _run_pip([*args, *reversed(_PACKAGE_NAMES)])
159228

160229

161230
def _main(argv=None):

Lib/test/test_ensurepip.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,68 @@
1-
import unittest
2-
import unittest.mock
3-
import test.support
1+
import contextlib
42
import os
53
import os.path
6-
import contextlib
74
import sys
5+
import tempfile
6+
import test.support
7+
import unittest
8+
import unittest.mock
89

910
import ensurepip
1011
import ensurepip._uninstall
1112

1213

13-
class TestEnsurePipVersion(unittest.TestCase):
14+
class TestPackages(unittest.TestCase):
15+
def touch(self, directory, filename):
16+
fullname = os.path.join(directory, filename)
17+
open(fullname, "wb").close()
18+
19+
def test_version(self):
20+
# Test version()
21+
with tempfile.TemporaryDirectory() as tmpdir:
22+
self.touch(tmpdir, "pip-1.2.3b1-py2.py3-none-any.whl")
23+
self.touch(tmpdir, "setuptools-49.1.3-py3-none-any.whl")
24+
with unittest.mock.patch.object(ensurepip, '_PACKAGES', None):
25+
with unittest.mock.patch.object(ensurepip, '_WHEEL_PKG_DIR', tmpdir):
26+
self.assertEqual(ensurepip.version(), '1.2.3b1')
27+
28+
def test_get_packages_no_dir(self):
29+
# Test _get_packages() without a wheel package directory
30+
with unittest.mock.patch.object(ensurepip, '_PACKAGES', None):
31+
with unittest.mock.patch.object(ensurepip, '_WHEEL_PKG_DIR', None):
32+
packages = ensurepip._get_packages()
33+
34+
# when bundled wheel packages are used, we get _PIP_VERSION
35+
self.assertEqual(ensurepip._PIP_VERSION, ensurepip.version())
36+
37+
# use bundled wheel packages
38+
self.assertIsNotNone(packages['pip'].wheel_name)
39+
self.assertIsNotNone(packages['setuptools'].wheel_name)
40+
41+
def test_get_packages_with_dir(self):
42+
# Test _get_packages() with a wheel package directory
43+
setuptools_filename = "setuptools-49.1.3-py3-none-any.whl"
44+
pip_filename = "pip-20.2.2-py2.py3-none-any.whl"
45+
46+
with tempfile.TemporaryDirectory() as tmpdir:
47+
self.touch(tmpdir, setuptools_filename)
48+
self.touch(tmpdir, pip_filename)
49+
# not used, make sure that it's ignored
50+
self.touch(tmpdir, "wheel-0.34.2-py2.py3-none-any.whl")
51+
52+
with unittest.mock.patch.object(ensurepip, '_PACKAGES', None):
53+
with unittest.mock.patch.object(ensurepip, '_WHEEL_PKG_DIR', tmpdir):
54+
packages = ensurepip._get_packages()
55+
56+
self.assertEqual(packages['setuptools'].version, '49.1.3')
57+
self.assertEqual(packages['setuptools'].wheel_path,
58+
os.path.join(tmpdir, setuptools_filename))
59+
self.assertEqual(packages['pip'].version, '20.2.2')
60+
self.assertEqual(packages['pip'].wheel_path,
61+
os.path.join(tmpdir, pip_filename))
62+
63+
# wheel package is ignored
64+
self.assertEqual(sorted(packages), ['pip', 'setuptools'])
1465

15-
def test_returns_version(self):
16-
self.assertEqual(ensurepip._PIP_VERSION, ensurepip.version())
1766

1867
class EnsurepipMixin:
1968

@@ -27,6 +76,8 @@ def setUp(self):
2776
real_devnull = os.devnull
2877
os_patch = unittest.mock.patch("ensurepip.os")
2978
patched_os = os_patch.start()
79+
# But expose os.listdir() used by _find_packages()
80+
patched_os.listdir = os.listdir
3081
self.addCleanup(os_patch.stop)
3182
patched_os.devnull = real_devnull
3283
patched_os.path = os.path
@@ -147,7 +198,7 @@ def test_pip_config_file_disabled(self):
147198
self.assertEqual(self.os_environ["PIP_CONFIG_FILE"], os.devnull)
148199

149200
@contextlib.contextmanager
150-
def fake_pip(version=ensurepip._PIP_VERSION):
201+
def fake_pip(version=ensurepip.version()):
151202
if version is None:
152203
pip = None
153204
else:
@@ -243,7 +294,7 @@ def test_pip_config_file_disabled(self):
243294

244295
# Basic testing of the main functions and their argument parsing
245296

246-
EXPECTED_VERSION_OUTPUT = "pip " + ensurepip._PIP_VERSION
297+
EXPECTED_VERSION_OUTPUT = "pip " + ensurepip.version()
247298

248299
class TestBootstrappingMainFunction(EnsurepipMixin, unittest.TestCase):
249300

Makefile.pre.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ INCLUDEDIR= @includedir@
145145
CONFINCLUDEDIR= $(exec_prefix)/include
146146
SCRIPTDIR= $(prefix)/lib
147147
ABIFLAGS= @ABIFLAGS@
148+
# Variable used by ensurepip
149+
WHEEL_PKG_DIR= @WHEEL_PKG_DIR@
148150

149151
# Detailed destination directories
150152
LIBDEST= $(SCRIPTDIR)/python$(VERSION)

configure

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ OPENSSL_INCLUDES
629629
ENSUREPIP
630630
SRCDIRS
631631
THREADHEADERS
632+
WHEEL_PKG_DIR
632633
LIBPL
633634
PY_ENABLE_SHARED
634635
LIBPYTHON
@@ -841,6 +842,7 @@ with_dtrace
841842
with_libm
842843
with_libc
843844
enable_big_digits
845+
with_wheel_pkg_dir
844846
with_computed_gotos
845847
with_ensurepip
846848
with_openssl
@@ -1554,6 +1556,9 @@ Optional Packages:
15541556
--with(out)-dtrace disable/enable DTrace support
15551557
--with-libm=STRING math library
15561558
--with-libc=STRING C library
1559+
--with-wheel-pkg-dir=PATH
1560+
Directory of wheel packages used by ensurepip
1561+
(default: none)
15571562
--with(out)-computed-gotos
15581563
Use computed gotos in evaluation loop (enabled by
15591564
default on supported compilers)
@@ -15249,6 +15254,29 @@ else
1524915254
fi
1525015255

1525115256

15257+
# Check for --with-wheel-pkg-dir=PATH
15258+
15259+
WHEEL_PKG_DIR=""
15260+
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for --with-wheel-pkg-dir" >&5
15261+
$as_echo_n "checking for --with-wheel-pkg-dir... " >&6; }
15262+
15263+
# Check whether --with-wheel-pkg-dir was given.
15264+
if test "${with_wheel_pkg_dir+set}" = set; then :
15265+
withval=$with_wheel_pkg_dir;
15266+
if test -n "$withval"; then
15267+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
15268+
$as_echo "yes" >&6; }
15269+
WHEEL_PKG_DIR="$withval"
15270+
else
15271+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
15272+
$as_echo "no" >&6; }
15273+
fi
15274+
else
15275+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
15276+
$as_echo "no" >&6; }
15277+
fi
15278+
15279+
1525215280
# Check whether right shifting a negative integer extends the sign bit
1525315281
# or fills with zeros (like the Cray J90, according to Tim Peters).
1525415282
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether right shift extends the sign bit" >&5

configure.ac

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4730,6 +4730,22 @@ else
47304730
fi
47314731
AC_SUBST(LIBPL)
47324732

4733+
# Check for --with-wheel-pkg-dir=PATH
4734+
AC_SUBST(WHEEL_PKG_DIR)
4735+
WHEEL_PKG_DIR=""
4736+
AC_MSG_CHECKING(for --with-wheel-pkg-dir)
4737+
AC_ARG_WITH(wheel-pkg-dir,
4738+
AS_HELP_STRING([--with-wheel-pkg-dir=PATH],
4739+
[Directory of wheel packages used by ensurepip (default: none)]),
4740+
[
4741+
if test -n "$withval"; then
4742+
AC_MSG_RESULT(yes)
4743+
WHEEL_PKG_DIR="$withval"
4744+
else
4745+
AC_MSG_RESULT(no)
4746+
fi],
4747+
[AC_MSG_RESULT(no)])
4748+
47334749
# Check whether right shifting a negative integer extends the sign bit
47344750
# or fills with zeros (like the Cray J90, according to Tim Peters).
47354751
AC_MSG_CHECKING(whether right shift extends the sign bit)

0 commit comments

Comments
 (0)