Imported Upstream version 49.3.0 upstream/49.3.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 29 Dec 2020 22:07:43 +0000 (07:07 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 29 Dec 2020 22:07:43 +0000 (07:07 +0900)
14 files changed:
.bumpversion.cfg
.github/workflows/python-tests.yml
CHANGES.rst
_distutils_hack/__init__.py [new file with mode: 0644]
_distutils_hack/override.py [new file with mode: 0644]
conftest.py
setup.cfg
setup.py
setuptools/__init__.py
setuptools/distutils_patch.py [deleted file]
setuptools/monkey.py
setuptools/sandbox.py
setuptools/tests/requirements.txt
setuptools/tests/test_distutils_adoption.py [new file with mode: 0644]

index b534af1bc13039ac75b1c3ebeddda662258a462b..99e5def842edb5b79bb63c902dc28c96e755292a 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 49.2.1
+current_version = 49.3.0
 commit = True
 tag = True
 
index f2188d38da1637d5318d4ea9df57e99fe591771f..93ec79d4ab777e6103c3de3dca88d1f0c5710f6d 100644 (file)
@@ -25,32 +25,54 @@ jobs:
         - 3.6
         - 3.5
         os:
-        - ubuntu-latest
+        - ubuntu-18.04
         - ubuntu-16.04
         - macOS-latest
         # - windows-2019
         # - windows-2016
         include:
-        # Dev versions
-        - { python-version: 3.9-dev, os: ubuntu-20.04 }
+        # Pre-release versions (GH-shipped)
+        - os: ubuntu-20.04
+          python-version: 3.9.0-beta.4 - 3.9.0
+        # Pre-release versions (deadsnakes)
+        - os: ubuntu-20.04
+          python-version: 3.9-beta
+        # Dev versions (deadsnakes)
+        - os: ubuntu-20.04
+          python-version: 3.9-dev
+        - os: ubuntu-20.04
+          python-version: 3.8-dev
 
     env:
       NETWORK_REQUIRED: 1
+      PYTHON_VERSION: ${{ matrix.python-version }}
       TOX_PARALLEL_NO_SPINNER: 1
       TOXENV: python
+      USE_DEADSNAKES: false
 
     steps:
     - uses: actions/checkout@master
-    - name: Set up Python ${{ matrix.python-version }} (deadsnakes)
+    - name: Set flag to use deadsnakes
+      if: >-
+        endsWith(env.PYTHON_VERSION, '-beta') || 
+        endsWith(env.PYTHON_VERSION, '-dev')
+      run: |
+        from __future__ import print_function
+        python_version = '${{ env.PYTHON_VERSION }}'.replace('-beta', '')
+        print('::set-env name=PYTHON_VERSION::{ver}'.format(ver=python_version))
+        print('::set-env name=USE_DEADSNAKES::true')
+      shell: python
+    - name: Set up Python ${{ env.PYTHON_VERSION }} (deadsnakes)
       uses: deadsnakes/action@v1.0.0
-      if: endsWith(matrix.python-version, '-dev')
+      if: fromJSON(env.USE_DEADSNAKES) && true || false
       with:
-        python-version: ${{ matrix.python-version }}
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v1.1.1
-      if: "!endsWith(matrix.python-version, '-dev')"
+        python-version: ${{ env.PYTHON_VERSION }}
+    - name: Set up Python ${{ env.PYTHON_VERSION }}
+      uses: actions/setup-python@v2.1.1
+      if: >-
+        !fromJSON(env.USE_DEADSNAKES) && true || false
       with:
-        python-version: ${{ matrix.python-version }}
+        python-version: ${{ env.PYTHON_VERSION }}
     - name: Log Python version
       run: >-
         python --version
@@ -82,9 +104,9 @@ jobs:
       run: >-
         python -m pip freeze --all
     - name: Adjust TOXENV for PyPy
-      if: startsWith(matrix.python-version, 'pypy')
+      if: startsWith(env.PYTHON_VERSION, 'pypy')
       run: >-
-        echo "::set-env name=TOXENV::${{ matrix.python-version }}"
+        echo "::set-env name=TOXENV::${{ env.PYTHON_VERSION }}"
     - name: Log env vars
       run: >-
         env
@@ -100,6 +122,7 @@ jobs:
         python -m
         tox
         --parallel auto
+        --parallel-live
         --notest
         --skip-missing-interpreters false
     - name: Test with tox
@@ -107,3 +130,6 @@ jobs:
         python -m
         tox
         --parallel auto
+        --parallel-live
+        --
+        -vvvvv
index 519330ebe9b43b0042ea8b456d2a26c74f8f4390..da0eada3f2b60e8b313895ba08129be0c76c5b4b 100644 (file)
@@ -1,3 +1,9 @@
+v49.3.0
+-------
+
+* #2259: 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.
+
+
 v49.2.1
 -------
 
diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py
new file mode 100644 (file)
index 0000000..71fa7ce
--- /dev/null
@@ -0,0 +1,96 @@
+import sys
+import os
+import re
+import importlib
+import warnings
+
+
+is_pypy = '__pypy__' in sys.builtin_module_names
+
+
+def warn_distutils_present():
+    if 'distutils' not in sys.modules:
+        return
+    if is_pypy and sys.version_info < (3, 7):
+        # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
+        # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
+        return
+    warnings.warn(
+        "Distutils was imported before Setuptools. This usage is discouraged "
+        "and may exhibit undesirable behaviors or errors. Please use "
+        "Setuptools' objects directly or at least import Setuptools first.")
+
+
+def clear_distutils():
+    if 'distutils' not in sys.modules:
+        return
+    warnings.warn("Setuptools is replacing distutils.")
+    mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
+    for name in mods:
+        del sys.modules[name]
+
+
+def enabled():
+    """
+    Allow selection of distutils by environment variable.
+    """
+    which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
+    return which == 'local'
+
+
+def ensure_local_distutils():
+    clear_distutils()
+    distutils = importlib.import_module('setuptools._distutils')
+    distutils.__name__ = 'distutils'
+    sys.modules['distutils'] = distutils
+
+    # sanity check that submodules load as expected
+    core = importlib.import_module('distutils.core')
+    assert '_distutils' in core.__file__, core.__file__
+
+
+def do_override():
+    """
+    Ensure that the local copy of distutils is preferred over stdlib.
+
+    See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
+    for more motivation.
+    """
+    warn_distutils_present()
+    if enabled():
+        ensure_local_distutils()
+
+
+class DistutilsMetaFinder:
+    def find_spec(self, fullname, path, target=None):
+        if path is not None or fullname != "distutils":
+            return None
+
+        return self.get_distutils_spec()
+
+    def get_distutils_spec(self):
+        import importlib.util
+
+        class DistutilsLoader(importlib.util.abc.Loader):
+
+            def create_module(self, spec):
+                return importlib.import_module('._distutils', 'setuptools')
+
+            def exec_module(self, module):
+                pass
+
+        return importlib.util.spec_from_loader('distutils', DistutilsLoader())
+
+
+DISTUTILS_FINDER = DistutilsMetaFinder()
+
+
+def add_shim():
+    sys.meta_path.insert(0, DISTUTILS_FINDER)
+
+
+def remove_shim():
+    try:
+        sys.meta_path.remove(DISTUTILS_FINDER)
+    except ValueError:
+        pass
diff --git a/_distutils_hack/override.py b/_distutils_hack/override.py
new file mode 100644 (file)
index 0000000..2cc433a
--- /dev/null
@@ -0,0 +1 @@
+__import__('_distutils_hack').do_override()
index 6013e1870f665e7a0b49868c91c0decfb8d0c770..25537f56e5cf9254e3e3ea3a0020e89b8023373b 100644 (file)
@@ -15,7 +15,7 @@ collect_ignore = [
     'tests/manual_test.py',
     'setuptools/tests/mod_with_constant.py',
     'setuptools/_distutils',
-    'setuptools/distutils_patch.py',
+    '_distutils_hack',
 ]
 
 
index 485714a3b64f4c59d318a3cb747a46035b72f7b8..fd3b11e6c6f02466924d459d49d7610f19d8f5a1 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,7 +16,7 @@ formats = zip
 
 [metadata]
 name = setuptools
-version = 49.2.1
+version = 49.3.0
 description = Easily download, build, install, upgrade, and uninstall Python packages
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
@@ -72,6 +72,7 @@ tests =
     paver; python_version>="3.6"
     futures; python_version=="2.7"
     pip>=19.1 # For proper file:// URLs support.
+    jaraco.envs
 
 docs =
     # Keep these in sync with docs/requirements.txt
index 1fe18bd13c4cccd1021e36d6f801fa52effb7543..5d98c029c58fe8101c3d5376c26a307b202d8dce 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -5,8 +5,10 @@ Distutils setup file, used to install or test 'setuptools'
 
 import os
 import sys
+import textwrap
 
 import setuptools
+from setuptools.command.install import install
 
 here = os.path.dirname(__file__)
 
@@ -81,8 +83,47 @@ def pypi_link(pkg_filename):
     return '/'.join(parts)
 
 
+class install_with_pth(install):
+    """
+    Custom install command to install a .pth file for distutils patching.
+
+    This hack is necessary because there's no standard way to install behavior
+    on startup (and it's debatable if there should be one). This hack (ab)uses
+    the `extra_path` behavior in Setuptools to install a `.pth` file with
+    implicit behavior on startup to give higher precedence to the local version
+    of `distutils` over the version from the standard library.
+
+    Please do not replicate this behavior.
+    """
+
+    _pth_name = 'distutils-precedence'
+    _pth_contents = textwrap.dedent("""
+        import os
+        enabled = os.environ.get('SETUPTOOLS_USE_DISTUTILS') == 'local'
+        enabled and __import__('_distutils_hack').add_shim()
+        """).lstrip().replace('\n', '; ')
+
+    def initialize_options(self):
+        install.initialize_options(self)
+        self.extra_path = self._pth_name, self._pth_contents
+
+    def finalize_options(self):
+        install.finalize_options(self)
+        self._restore_install_lib()
+
+    def _restore_install_lib(self):
+        """
+        Undo secondary effect of `extra_path` adding to `install_lib`
+        """
+        suffix = os.path.relpath(self.install_lib, self.install_libbase)
+
+        if suffix.strip() == self._pth_contents.strip():
+            self.install_lib = self.install_libbase
+
+
 setup_params = dict(
     src_root=None,
+    cmdclass={'install': install_with_pth},
     package_data=package_data,
     entry_points={
         "distutils.commands": [
index 8388251115d2fd2dd5e83722904feea6003f6162..99094230d39d35fcb1c23245d79025a297ceebba 100644 (file)
@@ -1,17 +1,15 @@
 """Extensions to the 'distutils' for large or complex distributions"""
 
-import os
+from fnmatch import fnmatchcase
 import functools
+import os
+import re
 
-# Disabled for now due to: #2228, #2230
-import setuptools.distutils_patch  # noqa: F401
+import _distutils_hack.override  # noqa: F401
 
 import distutils.core
-import distutils.filelist
-import re
 from distutils.errors import DistutilsOptionError
 from distutils.util import convert_path
-from fnmatch import fnmatchcase
 
 from ._deprecation_warning import SetuptoolsDeprecationWarning
 
diff --git a/setuptools/distutils_patch.py b/setuptools/distutils_patch.py
deleted file mode 100644 (file)
index 33f1e7f..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-"""
-Ensure that the local copy of distutils is preferred over stdlib.
-
-See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
-for more motivation.
-"""
-
-import sys
-import re
-import os
-import importlib
-import warnings
-
-
-is_pypy = '__pypy__' in sys.builtin_module_names
-
-
-def warn_distutils_present():
-    if 'distutils' not in sys.modules:
-        return
-    if is_pypy and sys.version_info < (3, 7):
-        # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
-        # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
-        return
-    warnings.warn(
-        "Distutils was imported before Setuptools. This usage is discouraged "
-        "and may exhibit undesirable behaviors or errors. Please use "
-        "Setuptools' objects directly or at least import Setuptools first.")
-
-
-def clear_distutils():
-    if 'distutils' not in sys.modules:
-        return
-    warnings.warn("Setuptools is replacing distutils.")
-    mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
-    for name in mods:
-        del sys.modules[name]
-
-
-def enabled():
-    """
-    Allow selection of distutils by environment variable.
-    """
-    which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
-    return which == 'local'
-
-
-def ensure_local_distutils():
-    clear_distutils()
-    distutils = importlib.import_module('setuptools._distutils')
-    distutils.__name__ = 'distutils'
-    sys.modules['distutils'] = distutils
-
-    # sanity check that submodules load as expected
-    core = importlib.import_module('distutils.core')
-    assert '_distutils' in core.__file__, core.__file__
-
-
-warn_distutils_present()
-if enabled():
-    ensure_local_distutils()
index 3c77f8cf27f0ab1e71d64cfc114ef9d1bf72295c..e5f1377b545f5475f6365a632084139691c9204b 100644 (file)
@@ -138,7 +138,7 @@ def patch_for_msvc_specialized_compiler():
     msvc = import_module('setuptools.msvc')
 
     if platform.system() != 'Windows':
-        # Compilers only availables on Microsoft Windows
+        # Compilers only available on Microsoft Windows
         return
 
     def patch_params(mod_name, func_name):
index 93ae8eb4d94f01ce02792ac0461878f2593b36d5..24a360808afeb76df73953f0751f7ad78feaa76a 100644 (file)
@@ -185,8 +185,8 @@ def setup_context(setup_dir):
     temp_dir = os.path.join(setup_dir, 'temp')
     with save_pkg_resources_state():
         with save_modules():
-            hide_setuptools()
             with save_path():
+                hide_setuptools()
                 with save_argv():
                     with override_temp(temp_dir):
                         with pushd(setup_dir):
@@ -195,6 +195,15 @@ def setup_context(setup_dir):
                             yield
 
 
+_MODULES_TO_HIDE = {
+    'setuptools',
+    'distutils',
+    'pkg_resources',
+    'Cython',
+    '_distutils_hack',
+}
+
+
 def _needs_hiding(mod_name):
     """
     >>> _needs_hiding('setuptools')
@@ -212,8 +221,8 @@ def _needs_hiding(mod_name):
     >>> _needs_hiding('Cython')
     True
     """
-    pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)')
-    return bool(pattern.match(mod_name))
+    base_module = mod_name.split('.', 1)[0]
+    return base_module in _MODULES_TO_HIDE
 
 
 def hide_setuptools():
@@ -223,6 +232,10 @@ def hide_setuptools():
     necessary to avoid issues such as #315 where setuptools upgrading itself
     would fail to find a function declared in the metadata.
     """
+    _distutils_hack = sys.modules.get('_distutils_hack', None)
+    if _distutils_hack is not None:
+        _distutils_hack.remove_shim()
+
     modules = filter(_needs_hiding, sys.modules)
     _clear_modules(modules)
 
index 19bf5aefd0b823160726c4405a73677d71595a99..d0d07f70c046bdf568efb42d247fe33ef2b1d428 100644 (file)
@@ -10,3 +10,4 @@ pytest-cov>=2.5.1
 paver; python_version>="3.6"
 futures; python_version=="2.7"
 pip>=19.1 # For proper file:// URLs support.
+jaraco.envs
diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py
new file mode 100644 (file)
index 0000000..daccc47
--- /dev/null
@@ -0,0 +1,70 @@
+import os
+import sys
+import functools
+import subprocess
+import platform
+
+import pytest
+import jaraco.envs
+import path
+
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+
+class VirtualEnv(jaraco.envs.VirtualEnv):
+    name = '.env'
+
+    def run(self, cmd, *args, **kwargs):
+        cmd = [self.exe(cmd[0])] + cmd[1:]
+        return subprocess.check_output(cmd, *args, cwd=self.root, **kwargs)
+
+
+@pytest.fixture
+def venv(tmpdir):
+    env = VirtualEnv()
+    env.root = path.Path(tmpdir)
+    env.req = os.getcwd()
+    return env.create()
+
+
+def popen_text(call):
+    """
+    Augment the Popen call with the parameters to ensure unicode text.
+    """
+    return functools.partial(call, universal_newlines=True) \
+        if sys.version_info < (3, 7) else functools.partial(call, text=True)
+
+
+def find_distutils(venv, imports='distutils', env=None, **kwargs):
+    py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals())
+    cmd = ['python', '-c', py_cmd]
+    if platform.system() == 'Windows':
+        env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
+    return popen_text(venv.run)(cmd, env=env, **kwargs)
+
+
+def test_distutils_stdlib(venv):
+    """
+    Ensure stdlib distutils is used when appropriate.
+    """
+    assert venv.name not in find_distutils(venv, env=dict()).split(os.sep)
+
+
+def test_distutils_local_with_setuptools(venv):
+    """
+    Ensure local distutils is used when appropriate.
+    """
+    env = dict(SETUPTOOLS_USE_DISTUTILS='local')
+    loc = find_distutils(venv, imports='setuptools, distutils', env=env)
+    assert venv.name in loc.split(os.sep)
+
+
+@pytest.mark.xfail('IS_PYPY', reason='pypy imports distutils on startup')
+def test_distutils_local(venv):
+    """
+    Even without importing, the setuptools-local copy of distutils is
+    preferred.
+    """
+    env = dict(SETUPTOOLS_USE_DISTUTILS='local')
+    assert venv.name in find_distutils(venv, env=env).split(os.sep)