Skip to content

Commit 00e46c2

Browse files
authored
Merge pull request #2305 from pypa/distutils-import-hack
Prefer included distutils even without importing setuptools. Closes #2259.
2 parents 59e116c + 7cf009a commit 00e46c2

File tree

8 files changed

+118
-22
lines changed

8 files changed

+118
-22
lines changed

setuptools/distutils_patch.py renamed to _distutils_hack/__init__.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
"""
2-
Ensure that the local copy of distutils is preferred over stdlib.
3-
4-
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
5-
for more motivation.
6-
"""
7-
81
import sys
9-
import re
102
import os
3+
import re
114
import importlib
125
import warnings
136

@@ -56,6 +49,48 @@ def ensure_local_distutils():
5649
assert '_distutils' in core.__file__, core.__file__
5750

5851

59-
warn_distutils_present()
60-
if enabled():
61-
ensure_local_distutils()
52+
def do_override():
53+
"""
54+
Ensure that the local copy of distutils is preferred over stdlib.
55+
56+
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
57+
for more motivation.
58+
"""
59+
warn_distutils_present()
60+
if enabled():
61+
ensure_local_distutils()
62+
63+
64+
class DistutilsMetaFinder:
65+
def find_spec(self, fullname, path, target=None):
66+
if path is not None or fullname != "distutils":
67+
return None
68+
69+
return self.get_distutils_spec()
70+
71+
def get_distutils_spec(self):
72+
import importlib.util
73+
74+
class DistutilsLoader(importlib.util.abc.Loader):
75+
76+
def create_module(self, spec):
77+
return importlib.import_module('._distutils', 'setuptools')
78+
79+
def exec_module(self, module):
80+
pass
81+
82+
return importlib.util.spec_from_loader('distutils', DistutilsLoader())
83+
84+
85+
DISTUTILS_FINDER = DistutilsMetaFinder()
86+
87+
88+
def add_shim():
89+
sys.meta_path.insert(0, DISTUTILS_FINDER)
90+
91+
92+
def remove_shim():
93+
try:
94+
sys.meta_path.remove(DISTUTILS_FINDER)
95+
except ValueError:
96+
pass

_distutils_hack/override.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__import__('_distutils_hack').do_override()

changelog.d/2259.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Setuptools now provides a .pth file (except for editable installs of setuptools) to the target environment to ensure that when enabled, the setuptools-provided distutils is preferred before setuptools has been imported (and even if setuptools is never imported). Honors the SETUPTOOLS_USE_DISTUTILS environment variable.

conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def pytest_addoption(parser):
1515
'tests/manual_test.py',
1616
'setuptools/tests/mod_with_constant.py',
1717
'setuptools/_distutils',
18-
'setuptools/distutils_patch.py',
18+
'_distutils_hack',
1919
]
2020

2121

setup.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
import os
77
import sys
8+
import textwrap
89

910
import setuptools
11+
from setuptools.command.install import install
1012

1113
here = os.path.dirname(__file__)
1214

@@ -81,8 +83,47 @@ def pypi_link(pkg_filename):
8183
return '/'.join(parts)
8284

8385

86+
class install_with_pth(install):
87+
"""
88+
Custom install command to install a .pth file for distutils patching.
89+
90+
This hack is necessary because there's no standard way to install behavior
91+
on startup (and it's debatable if there should be one). This hack (ab)uses
92+
the `extra_path` behavior in Setuptools to install a `.pth` file with
93+
implicit behavior on startup to give higher precedence to the local version
94+
of `distutils` over the version from the standard library.
95+
96+
Please do not replicate this behavior.
97+
"""
98+
99+
_pth_name = 'distutils-precedence'
100+
_pth_contents = textwrap.dedent("""
101+
import os
102+
enabled = os.environ.get('SETUPTOOLS_USE_DISTUTILS') == 'local'
103+
enabled and __import__('_distutils_hack').add_shim()
104+
""").lstrip().replace('\n', '; ')
105+
106+
def initialize_options(self):
107+
install.initialize_options(self)
108+
self.extra_path = self._pth_name, self._pth_contents
109+
110+
def finalize_options(self):
111+
install.finalize_options(self)
112+
self._restore_install_lib()
113+
114+
def _restore_install_lib(self):
115+
"""
116+
Undo secondary effect of `extra_path` adding to `install_lib`
117+
"""
118+
suffix = os.path.relpath(self.install_lib, self.install_libbase)
119+
120+
if suffix.strip() == self._pth_contents.strip():
121+
self.install_lib = self.install_libbase
122+
123+
84124
setup_params = dict(
85125
src_root=None,
126+
cmdclass={'install': install_with_pth},
86127
package_data=package_data,
87128
entry_points={
88129
"distutils.commands": [

setuptools/__init__.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"""Extensions to the 'distutils' for large or complex distributions"""
22

3-
import os
3+
from fnmatch import fnmatchcase
44
import functools
5+
import os
6+
import re
57

6-
# Disabled for now due to: #2228, #2230
7-
import setuptools.distutils_patch # noqa: F401
8+
import _distutils_hack.override # noqa: F401
89

910
import distutils.core
10-
import distutils.filelist
11-
import re
1211
from distutils.errors import DistutilsOptionError
1312
from distutils.util import convert_path
14-
from fnmatch import fnmatchcase
1513

1614
from ._deprecation_warning import SetuptoolsDeprecationWarning
1715

setuptools/sandbox.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ def setup_context(setup_dir):
185185
temp_dir = os.path.join(setup_dir, 'temp')
186186
with save_pkg_resources_state():
187187
with save_modules():
188-
hide_setuptools()
189188
with save_path():
189+
hide_setuptools()
190190
with save_argv():
191191
with override_temp(temp_dir):
192192
with pushd(setup_dir):
@@ -195,6 +195,15 @@ def setup_context(setup_dir):
195195
yield
196196

197197

198+
_MODULES_TO_HIDE = {
199+
'setuptools',
200+
'distutils',
201+
'pkg_resources',
202+
'Cython',
203+
'_distutils_hack',
204+
}
205+
206+
198207
def _needs_hiding(mod_name):
199208
"""
200209
>>> _needs_hiding('setuptools')
@@ -212,8 +221,8 @@ def _needs_hiding(mod_name):
212221
>>> _needs_hiding('Cython')
213222
True
214223
"""
215-
pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)')
216-
return bool(pattern.match(mod_name))
224+
base_module = mod_name.split('.', 1)[0]
225+
return base_module in _MODULES_TO_HIDE
217226

218227

219228
def hide_setuptools():
@@ -223,6 +232,10 @@ def hide_setuptools():
223232
necessary to avoid issues such as #315 where setuptools upgrading itself
224233
would fail to find a function declared in the metadata.
225234
"""
235+
_distutils_hack = sys.modules.get('_distutils_hack', None)
236+
if _distutils_hack is not None:
237+
_distutils_hack.remove_shim()
238+
226239
modules = filter(_needs_hiding, sys.modules)
227240
_clear_modules(modules)
228241

setuptools/tests/test_distutils_adoption.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import path
1010

1111

12+
IS_PYPY = '__pypy__' in sys.builtin_module_names
13+
14+
1215
class VirtualEnv(jaraco.envs.VirtualEnv):
1316
name = '.env'
1417

@@ -57,7 +60,11 @@ def test_distutils_local_with_setuptools(venv):
5760
assert venv.name in loc.split(os.sep)
5861

5962

60-
@pytest.mark.xfail(reason="#2259")
63+
@pytest.mark.xfail('IS_PYPY', reason='pypy imports distutils on startup')
6164
def test_distutils_local(venv):
65+
"""
66+
Even without importing, the setuptools-local copy of distutils is
67+
preferred.
68+
"""
6269
env = dict(SETUPTOOLS_USE_DISTUTILS='local')
6370
assert venv.name in find_distutils(venv, env=env).split(os.sep)

0 commit comments

Comments
 (0)