Imported Upstream version 62.0.0 upstream/62.0.0
authorJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:02:48 +0000 (17:02 +0900)
committerJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:02:48 +0000 (17:02 +0900)
15 files changed:
.bumpversion.cfg
CHANGES.rst
pkg_resources/__init__.py
pkg_resources/tests/test_working_set.py
setup.cfg
setuptools/archive_util.py
setuptools/command/dist_info.py
setuptools/command/easy_install.py
setuptools/command/egg_info.py
setuptools/config/pyprojecttoml.py
setuptools/tests/config/test_pyprojecttoml.py
setuptools/tests/test_dist_info.py
setuptools/tests/test_easy_install.py
setuptools/tests/test_wheel.py
setuptools/wheel.py

index 87fa5350ab4ec1c6d238aa3e6ba789a6764d6be6..5c2f2e45779d76a555dcb64496cceaedb0b4fac7 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.3.1
+current_version = 62.0.0
 commit = True
 tag = True
 
index 590f776630258d4adfb92497b29f3a8627dd154c..126457be2cdcc530fc352d41e5991fcf564abf07 100644 (file)
@@ -1,3 +1,23 @@
+v62.0.0
+-------
+
+
+Breaking Changes
+^^^^^^^^^^^^^^^^
+* #3151: Made ``setup.py develop --user`` install to the user site packages directory even if it is disabled in the current interpreter.
+
+Changes
+^^^^^^^
+* #3153: When resolving requirements use both canonical and normalized names -- by :user:`ldaniluk`
+* #3167: Honor unix file mode in ZipFile when installing wheel via ``install_as_egg`` -- by :user:`delijati`
+
+Misc
+^^^^
+* #3088: Fixed duplicated tag with the ``dist-info`` command.
+* #3247: Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
+  from being dynamically specified in ``setup.py``.
+
+
 v61.3.1
 -------
 
index 852476e250e7ff9df291eed7064872aaa03f8cf2..d59226af9d7fe1b5279e99ff6e333032d1cec274 100644 (file)
@@ -83,6 +83,7 @@ __import__('pkg_resources.extern.packaging.version')
 __import__('pkg_resources.extern.packaging.specifiers')
 __import__('pkg_resources.extern.packaging.requirements')
 __import__('pkg_resources.extern.packaging.markers')
+__import__('pkg_resources.extern.packaging.utils')
 
 if sys.version_info < (3, 5):
     raise RuntimeError("Python 3.5 or later is required")
@@ -554,6 +555,7 @@ class WorkingSet:
         self.entries = []
         self.entry_keys = {}
         self.by_key = {}
+        self.normalized_to_canonical_keys = {}
         self.callbacks = []
 
         if entries is None:
@@ -634,6 +636,14 @@ class WorkingSet:
         is returned.
         """
         dist = self.by_key.get(req.key)
+
+        if dist is None:
+            canonical_key = self.normalized_to_canonical_keys.get(req.key)
+
+            if canonical_key is not None:
+                req.key = canonical_key
+                dist = self.by_key.get(canonical_key)
+
         if dist is not None and dist not in req:
             # XXX add more info
             raise VersionConflict(dist, req)
@@ -702,6 +712,8 @@ class WorkingSet:
             return
 
         self.by_key[dist.key] = dist
+        normalized_name = packaging.utils.canonicalize_name(dist.key)
+        self.normalized_to_canonical_keys[normalized_name] = dist.key
         if dist.key not in keys:
             keys.append(dist.key)
         if dist.key not in keys2:
@@ -922,14 +934,15 @@ class WorkingSet:
     def __getstate__(self):
         return (
             self.entries[:], self.entry_keys.copy(), self.by_key.copy(),
-            self.callbacks[:]
+            self.normalized_to_canonical_keys.copy(), self.callbacks[:]
         )
 
-    def __setstate__(self, e_k_b_c):
-        entries, keys, by_key, callbacks = e_k_b_c
+    def __setstate__(self, e_k_b_n_c):
+        entries, keys, by_key, normalized_to_canonical_keys, callbacks = e_k_b_n_c
         self.entries = entries[:]
         self.entry_keys = keys.copy()
         self.by_key = by_key.copy()
+        self.normalized_to_canonical_keys = normalized_to_canonical_keys.copy()
         self.callbacks = callbacks[:]
 
 
index db13c7149bd83513291d24d1f1f9cd68289b3466..575656ee8f342d2451035d5250cef4b72c783c6e 100644 (file)
@@ -42,7 +42,7 @@ def parse_distributions(s):
             continue
         fields = spec.split('\n', 1)
         assert 1 <= len(fields) <= 2
-        name, version = fields.pop(0).split('-')
+        name, version = fields.pop(0).rsplit('-', 1)
         if fields:
             requires = textwrap.dedent(fields.pop(0))
             metadata = Metadata(('requires.txt', requires))
@@ -465,6 +465,25 @@ def parametrize_test_working_set_resolve(*test_list):
     # resolved [replace conflicting]
     VersionConflict
     ''',
+
+    '''
+    # id
+    wanted_normalized_name_installed_canonical
+
+    # installed
+    foo.bar-3.6
+
+    # installable
+
+    # wanted
+    foo-bar==3.6
+
+    # resolved
+    foo.bar-3.6
+
+    # resolved [replace conflicting]
+    foo.bar-3.6
+    ''',
 )
 def test_working_set_resolve(installed_dists, installable_dists, requirements,
                              replace_conflicting, resolved_dists_or_exception):
index 4f84656d7f73244515b1bd117a7f904a23cd9d36..78c088a15be8372fd8e22635c28a88148f85dd0d 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.3.1
+version = 62.0.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
index 73b2db752aa91c5d5a52e794a69a102ea369b3e6..d8e10c13e154802f4a742ed4904f0071369aa2ad 100644 (file)
@@ -100,29 +100,37 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter):
         raise UnrecognizedFormat("%s is not a zip file" % (filename,))
 
     with zipfile.ZipFile(filename) as z:
-        for info in z.infolist():
-            name = info.filename
+        _unpack_zipfile_obj(z, extract_dir, progress_filter)
 
-            # don't extract absolute paths or ones with .. in them
-            if name.startswith('/') or '..' in name.split('/'):
-                continue
 
-            target = os.path.join(extract_dir, *name.split('/'))
-            target = progress_filter(name, target)
-            if not target:
-                continue
-            if name.endswith('/'):
-                # directory
-                ensure_directory(target)
-            else:
-                # file
-                ensure_directory(target)
-                data = z.read(info.filename)
-                with open(target, 'wb') as f:
-                    f.write(data)
-            unix_attributes = info.external_attr >> 16
-            if unix_attributes:
-                os.chmod(target, unix_attributes)
+def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter):
+    """Internal/private API used by other parts of setuptools.
+    Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile`
+    object instead of a filename.
+    """
+    for info in zipfile_obj.infolist():
+        name = info.filename
+
+        # don't extract absolute paths or ones with .. in them
+        if name.startswith('/') or '..' in name.split('/'):
+            continue
+
+        target = os.path.join(extract_dir, *name.split('/'))
+        target = progress_filter(name, target)
+        if not target:
+            continue
+        if name.endswith('/'):
+            # directory
+            ensure_directory(target)
+        else:
+            # file
+            ensure_directory(target)
+            data = zipfile_obj.read(info.filename)
+            with open(target, 'wb') as f:
+                f.write(data)
+        unix_attributes = info.external_attr >> 16
+        if unix_attributes:
+            os.chmod(target, unix_attributes)
 
 
 def _resolve_tar_file_or_dir(tar_obj, tar_member_obj):
index c45258fa03a3ddd6a73db4514365f8741d16ca86..8b8509f3df10e15e00a43cf46e687cd50361d2cb 100644 (file)
@@ -4,9 +4,13 @@ As defined in the wheel specification
 """
 
 import os
+import re
+import warnings
+from inspect import cleandoc
 
 from distutils.core import Command
 from distutils import log
+from setuptools.extern import packaging
 
 
 class dist_info(Command):
@@ -29,8 +33,36 @@ class dist_info(Command):
         egg_info.egg_base = self.egg_base
         egg_info.finalize_options()
         egg_info.run()
-        dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'
+        name = _safe(self.distribution.get_name())
+        version = _version(self.distribution.get_version())
+        base = self.egg_base or os.curdir
+        dist_info_dir = os.path.join(base, f"{name}-{version}.dist-info")
         log.info("creating '{}'".format(os.path.abspath(dist_info_dir)))
 
         bdist_wheel = self.get_finalized_command('bdist_wheel')
         bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)
+
+
+def _safe(component: str) -> str:
+    """Escape a component used to form a wheel name according to PEP 491"""
+    return re.sub(r"[^\w\d.]+", "_", component)
+
+
+def _version(version: str) -> str:
+    """Convert an arbitrary string to a version string."""
+    v = version.replace(' ', '.')
+    try:
+        return str(packaging.version.Version(v)).replace("-", "_")
+    except packaging.version.InvalidVersion:
+        msg = f"""!!\n\n
+        ###################
+        # Invalid version #
+        ###################
+        {version!r} is not valid according to PEP 440.\n
+        Please make sure specify a valid version for your package.
+        Also note that future releases of setuptools may halt the build process
+        if an invalid version is given.
+        \n\n!!
+        """
+        warnings.warn(cleandoc(msg))
+        return _safe(v).strip("_")
index 77dcd25c66507e4cd9d9e06abdb7ed334a3f1f58..444d3b33110b65c14ff5a043d0ca4137e92b30eb 100644 (file)
@@ -169,12 +169,8 @@ class easy_install(Command):
         self.install_data = None
         self.install_base = None
         self.install_platbase = None
-        if site.ENABLE_USER_SITE:
-            self.install_userbase = site.USER_BASE
-            self.install_usersite = site.USER_SITE
-        else:
-            self.install_userbase = None
-            self.install_usersite = None
+        self.install_userbase = site.USER_BASE
+        self.install_usersite = site.USER_SITE
         self.no_find_links = None
 
         # Options not specifiable via command line
@@ -253,11 +249,9 @@ class easy_install(Command):
             getattr(sys, 'windir', '').replace('.', ''),
         )
 
-        if site.ENABLE_USER_SITE:
-            self.config_vars['userbase'] = self.install_userbase
-            self.config_vars['usersite'] = self.install_usersite
-
-        elif self.user:
+        self.config_vars['userbase'] = self.install_userbase
+        self.config_vars['usersite'] = self.install_usersite
+        if self.user and not site.ENABLE_USER_SITE:
             log.warn("WARNING: The user site-packages directory is disabled.")
 
         self._fix_install_dir_for_user_site()
@@ -375,7 +369,7 @@ class easy_install(Command):
         """
         Fix the install_dir if "--user" was used.
         """
-        if not self.user or not site.ENABLE_USER_SITE:
+        if not self.user:
             return
 
         self.create_home_path()
index 63389654af30f6fa7fbb52a0861c00c3b840ad48..c37ab81fa6382a521c2bad8fbe1c885aeaf654c6 100644 (file)
@@ -136,11 +136,21 @@ class InfoCommon:
         in which case the version string already contains all tags.
         """
         return (
-            version if self.vtags and version.endswith(self.vtags)
+            version if self.vtags and self._already_tagged(version)
             else version + self.vtags
         )
 
-    def tags(self):
+    def _already_tagged(self, version: str) -> bool:
+        # Depending on their format, tags may change with version normalization.
+        # So in addition the regular tags, we have to search for the normalized ones.
+        return version.endswith(self.vtags) or version.endswith(self._safe_tags())
+
+    def _safe_tags(self) -> str:
+        # To implement this we can rely on `safe_version` pretending to be version 0
+        # followed by tags. Then we simply discard the starting 0 (fake version number)
+        return safe_version(f"0{self.vtags}")[1:]
+
+    def tags(self) -> str:
         version = ''
         if self.tag_build:
             version += self.tag_build
index d4024956419c3656f17e88252fb1747afc9aa859..be812142e020b63d3fbe5edc67f7172f767707e9 100644 (file)
@@ -316,12 +316,17 @@ class _ConfigExpander:
         return None
 
     def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]:
-        if "readme" in self.dynamic:
-            dynamic_cfg = self.dynamic_cfg
+        if "readme" not in self.dynamic:
+            return None
+
+        dynamic_cfg = self.dynamic_cfg
+        if "readme" in dynamic_cfg:
             return {
                 "text": self._obtain(dist, "readme", {}),
                 "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
             }
+
+        self._ensure_previously_set(dist, "readme")
         return None
 
     def _obtain_entry_points(
index 4c2370140a4533aa58ee6df14a7c38b22c3deb89..200312b54d9eee38c05467d46337c0ac87d204f1 100644 (file)
@@ -253,6 +253,20 @@ class TestClassifiers:
         with pytest.raises(OptionError, match="No configuration .* .classifiers."):
             read_configuration(pyproject)
 
+    def test_dynamic_readme_from_setup_script_args(self, tmp_path):
+        config = """
+        [project]
+        name = "myproj"
+        version = '42'
+        dynamic = ["readme"]
+        """
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(cleandoc(config))
+        dist = Distribution(attrs={"long_description": "42"})
+        # No error should occur because of missing `readme`
+        dist = apply_configuration(dist, pyproject)
+        assert dist.metadata.long_description == "42"
+
     def test_dynamic_without_file(self, tmp_path):
         config = """
         [project]
index 29fbd09dbee462eff64bd2afab51c263c1e80cf5..813ef51d32c703688d27304678cab7e07aa0e0b5 100644 (file)
@@ -1,12 +1,21 @@
 """Test .dist-info style distributions.
 """
+import pathlib
+import re
+import subprocess
+import sys
+from functools import partial
 
 import pytest
 
 import pkg_resources
+from setuptools.archive_util import unpack_archive
 from .textwrap import DALS
 
 
+read = partial(pathlib.Path.read_text, encoding="utf-8")
+
+
 class TestDistInfo:
 
     metadata_base = DALS("""
@@ -72,3 +81,78 @@ class TestDistInfo:
                 pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'),
             ]
             assert d.extras == ['baz']
+
+    def test_invalid_version(self, tmp_path):
+        config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n"
+        (tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
+        msg = re.compile("invalid version", re.M | re.I)
+        output = run_command("dist_info", cwd=tmp_path)
+        assert msg.search(output)
+        dist_info = next(tmp_path.glob("*.dist-info"))
+        assert dist_info.name.startswith("proj-42")
+
+
+class TestWheelCompatibility:
+    """Make sure the .dist-info directory produced with the ``dist_info`` command
+    is the same as the one produced by ``bdist_wheel``.
+    """
+    SETUPCFG = DALS("""
+    [metadata]
+    name = {name}
+    version = {version}
+
+    [options]
+    install_requires = foo>=12; sys_platform != "linux"
+
+    [options.extras_require]
+    test = pytest
+
+    [options.entry_points]
+    console_scripts =
+        executable-name = my_package.module:function
+    discover =
+        myproj = my_package.other_module:function
+    """)
+
+    EGG_INFO_OPTS = [
+        # Related: #3088 #2872
+        ("", ""),
+        (".post", "[egg_info]\ntag_build = post\n"),
+        (".post", "[egg_info]\ntag_build = .post\n"),
+        (".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
+        (".dev", "[egg_info]\ntag_build = .dev\n"),
+        (".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
+        ("a1", "[egg_info]\ntag_build = .a1\n"),
+        ("+local", "[egg_info]\ntag_build = +local\n"),
+    ]
+
+    @pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split())
+    @pytest.mark.parametrize("version", ["0.42.13"])
+    @pytest.mark.parametrize("suffix, cfg", EGG_INFO_OPTS)
+    def test_dist_info_is_the_same_as_in_wheel(
+        self, name, version, tmp_path, suffix, cfg
+    ):
+        config = self.SETUPCFG.format(name=name, version=version) + cfg
+
+        for i in "dir_wheel", "dir_dist":
+            (tmp_path / i).mkdir()
+            (tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8")
+
+        run_command("bdist_wheel", cwd=tmp_path / "dir_wheel")
+        wheel = next(tmp_path.glob("dir_wheel/dist/*.whl"))
+        unpack_archive(wheel, tmp_path / "unpack")
+        wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info"))
+
+        run_command("dist_info", cwd=tmp_path / "dir_dist")
+        dist_info = next(tmp_path.glob("dir_dist/*.dist-info"))
+
+        assert dist_info.name == wheel_dist_info.name
+        assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}")
+        for file in "METADATA", "entry_points.txt":
+            assert read(dist_info / file) == read(wheel_dist_info / file)
+
+
+def run_command(*cmd, **kwargs):
+    opts = {"stderr": subprocess.STDOUT, "text": True, **kwargs}
+    cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *cmd]
+    return subprocess.check_output(cmd, **opts)
index 0d26dc7332a476782f5c9711e0530d9a49137265..726f9fda5a915e1b5f282567c0f4c3f9ac4c6b86 100644 (file)
@@ -19,6 +19,7 @@ import subprocess
 import pathlib
 import warnings
 from collections import namedtuple
+from pathlib import Path
 
 import pytest
 from jaraco import path
@@ -1166,3 +1167,52 @@ def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
     assert cmd.config_vars['py_version'] == '3.10.1'
     assert cmd.config_vars['py_version_short'] == '3.10'
     assert cmd.config_vars['py_version_nodot'] == '310'
+
+
+def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path):
+    ''' `setup.py develop` should honor `--user` even under build isolation'''
+
+    # == Arrange ==
+    # Pretend that build isolation was enabled
+    # e.g pip sets the environment varible PYTHONNOUSERSITE=1
+    monkeypatch.setattr('site.ENABLE_USER_SITE', False)
+
+    # Patching $HOME for 2 reasons:
+    # 1. setuptools/command/easy_install.py:create_home_path
+    #    tries creating directories in $HOME
+    # given `self.config_vars['DESTDIRS'] = "/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload"``  # noqa: E501
+    # it will `makedirs("/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")``  # noqa: E501
+    # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
+    #    To point inside our new home
+    monkeypatch.setenv('HOME', str(tmp_path / '.home'))
+    monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home'))
+    monkeypatch.setenv('APPDATA', str(tmp_path / '.home'))
+    monkeypatch.setattr('site.USER_BASE', None)
+    monkeypatch.setattr('site.USER_SITE', None)
+    user_site = Path(site.getusersitepackages())
+    user_site.mkdir(parents=True, exist_ok=True)
+
+    sys_prefix = (tmp_path / '.sys_prefix')
+    sys_prefix.mkdir(parents=True, exist_ok=True)
+    monkeypatch.setattr('sys.prefix', str(sys_prefix))
+
+    setup_script = (
+        "__import__('setuptools').setup(name='aproj', version=42, packages=[])\n"
+    )
+    (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
+
+    # == Sanity check ==
+    assert list(sys_prefix.glob("*")) == []
+    assert list(user_site.glob("*")) == []
+
+    # == Act ==
+    run_setup('setup.py', ['develop', '--user'])
+
+    # == Assert ==
+    # Should not install to sys.prefix
+    assert list(sys_prefix.glob("*")) == []
+    # Should install to user site
+    installed = {f.name for f in user_site.glob("*")}
+    # sometimes easy-install.pth is created and sometimes not
+    installed = installed - {"easy-install.pth"}
+    assert installed == {'aproj.egg-link'}
index a15c3a460dac5efa677a160148364afbb2ccd132..89d65d0b791cd63dc6100f59f9e081e47eeb936f 100644 (file)
@@ -6,6 +6,8 @@
 from distutils.sysconfig import get_config_var
 from distutils.util import get_platform
 import contextlib
+import pathlib
+import stat
 import glob
 import inspect
 import os
@@ -614,3 +616,88 @@ def test_wheel_is_compatible(monkeypatch):
     monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags)
     assert Wheel(
         'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()
+
+
+def test_wheel_mode():
+    @contextlib.contextmanager
+    def build_wheel(extra_file_defs=None, **kwargs):
+        file_defs = {
+            'setup.py': (DALS(
+                '''
+                # -*- coding: utf-8 -*-
+                from setuptools import setup
+                import setuptools
+                setup(**%r)
+                '''
+            ) % kwargs).encode('utf-8'),
+        }
+        if extra_file_defs:
+            file_defs.update(extra_file_defs)
+        with tempdir() as source_dir:
+            path.build(file_defs, source_dir)
+            runsh = pathlib.Path(source_dir) / "script.sh"
+            os.chmod(runsh, 0o777)
+            subprocess.check_call((sys.executable, 'setup.py',
+                                   '-q', 'bdist_wheel'), cwd=source_dir)
+            yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]
+
+    params = dict(
+        id='script',
+        file_defs={
+            'script.py': DALS(
+                '''
+                #/usr/bin/python
+                print('hello world!')
+                '''
+            ),
+            'script.sh': DALS(
+                '''
+                #/bin/sh
+                echo 'hello world!'
+                '''
+            ),
+        },
+        setup_kwargs=dict(
+            scripts=['script.py', 'script.sh'],
+        ),
+        install_tree=flatten_tree({
+            'foo-1.0-py{py_version}.egg': {
+                'EGG-INFO': [
+                    'PKG-INFO',
+                    'RECORD',
+                    'WHEEL',
+                    'top_level.txt',
+                    {'scripts': [
+                        'script.py',
+                        'script.sh'
+                    ]}
+
+                ]
+            }
+        })
+    )
+
+    project_name = params.get('name', 'foo')
+    version = params.get('version', '1.0')
+    install_tree = params.get('install_tree')
+    file_defs = params.get('file_defs', {})
+    setup_kwargs = params.get('setup_kwargs', {})
+
+    with build_wheel(
+        name=project_name,
+        version=version,
+        install_requires=[],
+        extras_require={},
+        extra_file_defs=file_defs,
+        **setup_kwargs
+    ) as filename, tempdir() as install_dir:
+        _check_wheel_install(filename, install_dir,
+                             install_tree, project_name,
+                             version, None)
+        w = Wheel(filename)
+        base = pathlib.Path(install_dir) / w.egg_name()
+        script_sh = base / "EGG-INFO" / "scripts" / "script.sh"
+        assert script_sh.exists()
+        if sys.platform != 'win32':
+            # Editable file mode has no effect on Windows
+            assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
index 9819e8b9fdfd0810d7e407334fd7f7d23e4944ff..0ced0ff2e24098e1d417f94c1df69f55d5103423 100644 (file)
@@ -15,6 +15,7 @@ from pkg_resources import parse_version
 from setuptools.extern.packaging.tags import sys_tags
 from setuptools.extern.packaging.utils import canonicalize_name
 from setuptools.command.egg_info import write_requirements
+from setuptools.archive_util import _unpack_zipfile_obj
 
 
 WHEEL_NAME = re.compile(
@@ -121,8 +122,7 @@ class Wheel:
             raise ValueError(
                 'unsupported wheel format version: %s' % wheel_version)
         # Extract to target directory.
-        os.mkdir(destination_eggdir)
-        zf.extractall(destination_eggdir)
+        _unpack_zipfile_obj(zf, destination_eggdir)
         # Convert metadata.
         dist_info = os.path.join(destination_eggdir, dist_info)
         dist = pkg_resources.Distribution.from_location(