Skip to content

Commit 75e59a9

Browse files
authored
bpo-42856: Add --with-wheel-pkg-dir=PATH configure option (GH-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.
1 parent c1c3493 commit 75e59a9

File tree

8 files changed

+220
-36
lines changed

8 files changed

+220
-36
lines changed

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.

Doc/whatsnew/3.10.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,18 @@ Build Changes
621621
don't build nor install test modules.
622622
(Contributed by Xavier de Gaye, Thomas Petazzoni and Peixing Xin in :issue:`27640`.)
623623

624+
* Add ``--with-wheel-pkg-dir=PATH`` option to the ``./configure`` script. If
625+
specified, the :mod:`ensurepip` module looks for ``setuptools`` and ``pip``
626+
wheel packages in this directory: if both are present, these wheel packages
627+
are used instead of ensurepip bundled wheel packages.
628+
629+
Some Linux distribution packaging policies recommend against bundling
630+
dependencies. For example, Fedora installs wheel packages in the
631+
``/usr/share/python-wheels/`` directory and don't install the
632+
``ensurepip._bundled`` package.
633+
634+
(Contributed by Victor Stinner in :issue:`42856`.)
635+
624636

625637
C API Changes
626638
=============

Lib/ensurepip/__init__.py

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,82 @@
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"]
14-
15-
13+
_PACKAGE_NAMES = ('setuptools', 'pip')
1614
_SETUPTOOLS_VERSION = "47.1.0"
17-
1815
_PIP_VERSION = "20.2.3"
19-
2016
_PROJECTS = [
2117
("setuptools", _SETUPTOOLS_VERSION, "py3"),
2218
("pip", _PIP_VERSION, "py2.py3"),
2319
]
2420

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.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+
2580

2681
def _run_pip(args, additional_paths=None):
2782
# Run the bootstraping in a subprocess to avoid leaking any state that happens
@@ -42,7 +97,8 @@ def version():
4297
"""
4398
Returns a string specifying the bundled version of pip.
4499
"""
45-
return _PIP_VERSION
100+
return _get_packages()['pip'].version
101+
46102

47103
def _disable_pip_configuration_settings():
48104
# We deliberately ignore all pip environment variables
@@ -104,16 +160,23 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
104160
# Put our bundled wheels into a temporary directory and construct the
105161
# additional paths that need added to sys.path
106162
additional_paths = []
107-
for project, version, py_tag in _PROJECTS:
108-
wheel_name = "{}-{}-{}-none-any.whl".format(project, version, py_tag)
109-
whl = resources.read_binary(
110-
_bundled,
111-
wheel_name,
112-
)
113-
with open(os.path.join(tmpdir, wheel_name), "wb") as fp:
163+
for name, package in _get_packages().items():
164+
if package.wheel_name:
165+
# Use bundled wheel package
166+
from ensurepip import _bundled
167+
wheel_name = package.wheel_name
168+
whl = resources.read_binary(_bundled, wheel_name)
169+
else:
170+
# Use the wheel package directory
171+
with open(package.wheel_path, "rb") as fp:
172+
whl = fp.read()
173+
wheel_name = os.path.basename(package.wheel_path)
174+
175+
filename = os.path.join(tmpdir, wheel_name)
176+
with open(filename, "wb") as fp:
114177
fp.write(whl)
115178

116-
additional_paths.append(os.path.join(tmpdir, wheel_name))
179+
additional_paths.append(filename)
117180

118181
# Construct the arguments to be passed to the pip command
119182
args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir]
@@ -126,7 +189,7 @@ def _bootstrap(*, root=None, upgrade=False, user=False,
126189
if verbosity:
127190
args += ["-" + "v" * verbosity]
128191

129-
return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
192+
return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)
130193

131194
def _uninstall_helper(*, verbosity=0):
132195
"""Helper to support a clean default uninstall process on Windows
@@ -139,11 +202,14 @@ def _uninstall_helper(*, verbosity=0):
139202
except ImportError:
140203
return
141204

142-
# If the pip version doesn't match the bundled one, leave it alone
143-
if pip.__version__ != _PIP_VERSION:
144-
msg = ("ensurepip will only uninstall a matching version "
145-
"({!r} installed, {!r} bundled)")
146-
print(msg.format(pip.__version__, _PIP_VERSION), file=sys.stderr)
205+
# If the installed pip version doesn't match the available one,
206+
# leave it alone
207+
available_version = version()
208+
if pip.__version__ != available_version:
209+
print(f"ensurepip will only uninstall a matching version "
210+
f"({pip.__version__!r} installed, "
211+
f"{available_version!r} available)",
212+
file=sys.stderr)
147213
return
148214

149215
_disable_pip_configuration_settings()
@@ -153,7 +219,7 @@ def _uninstall_helper(*, verbosity=0):
153219
if verbosity:
154220
args += ["-" + "v" * verbosity]
155221

156-
return _run_pip(args + [p[0] for p in reversed(_PROJECTS)])
222+
return _run_pip([*args, *reversed(_PACKAGE_NAMES)])
157223

158224

159225
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+
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+
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+
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
@@ -146,6 +146,8 @@ CONFINCLUDEDIR= $(exec_prefix)/include
146146
PLATLIBDIR= @PLATLIBDIR@
147147
SCRIPTDIR= $(prefix)/$(PLATLIBDIR)
148148
ABIFLAGS= @ABIFLAGS@
149+
# Variable used by ensurepip
150+
WHEEL_PKG_DIR= @WHEEL_PKG_DIR@
149151

150152
# Detailed destination directories
151153
BINLIBDEST= @BINLIBDEST@
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Add ``--with-wheel-pkg-dir=PATH`` option to the ``./configure`` script. If
2+
specified, the :mod:`ensurepip` module looks for ``setuptools`` and ``pip``
3+
wheel packages in this directory: if both are present, these wheel packages are
4+
used instead of ensurepip bundled wheel packages.
5+
6+
Some Linux distribution packaging policies recommend against bundling
7+
dependencies. For example, Fedora installs wheel packages in the
8+
``/usr/share/python-wheels/`` directory and don't install the
9+
``ensurepip._bundled`` package.

configure

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ OPENSSL_INCLUDES
630630
ENSUREPIP
631631
SRCDIRS
632632
THREADHEADERS
633+
WHEEL_PKG_DIR
633634
LIBPL
634635
PY_ENABLE_SHARED
635636
PLATLIBDIR
@@ -847,6 +848,7 @@ with_libm
847848
with_libc
848849
enable_big_digits
849850
with_platlibdir
851+
with_wheel_pkg_dir
850852
with_computed_gotos
851853
with_ensurepip
852854
with_openssl
@@ -1576,6 +1578,9 @@ Optional Packages:
15761578
system-dependent)
15771579
--with-platlibdir=DIRNAME
15781580
Python library directory name (default is "lib")
1581+
--with-wheel-pkg-dir=PATH
1582+
Directory of wheel packages used by ensurepip
1583+
(default: none)
15791584
--with-computed-gotos enable computed gotos in evaluation loop (enabled by
15801585
default on supported compilers)
15811586
--with-ensurepip[=install|upgrade|no]
@@ -15493,6 +15498,29 @@ else
1549315498
fi
1549415499

1549515500

15501+
# Check for --with-wheel-pkg-dir=PATH
15502+
15503+
WHEEL_PKG_DIR=""
15504+
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for --with-wheel-pkg-dir" >&5
15505+
$as_echo_n "checking for --with-wheel-pkg-dir... " >&6; }
15506+
15507+
# Check whether --with-wheel-pkg-dir was given.
15508+
if test "${with_wheel_pkg_dir+set}" = set; then :
15509+
withval=$with_wheel_pkg_dir;
15510+
if test -n "$withval"; then
15511+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
15512+
$as_echo "yes" >&6; }
15513+
WHEEL_PKG_DIR="$withval"
15514+
else
15515+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
15516+
$as_echo "no" >&6; }
15517+
fi
15518+
else
15519+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
15520+
$as_echo "no" >&6; }
15521+
fi
15522+
15523+
1549615524
# Check whether right shifting a negative integer extends the sign bit
1549715525
# or fills with zeros (like the Cray J90, according to Tim Peters).
1549815526
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether right shift extends the sign bit" >&5

0 commit comments

Comments
 (0)