Repository: pypa/wheel

Issue:
wheel 0.38.0 causes circular dependency with setuptools
Since wheel 0.38.0 was released just now, installing certain packages through pip is broken:

```
$ python3.11 -m pip install pyzmq==22.0.3 --install-option=--zmq=bundled
WARNING: Implying --no-binary=:all: due to the presence of --build-option / --global-option / --install-option. Consider using --config-settings for more flexibility.
DEPRECATION: --install-option is deprecated because it forces pip to use the 'setup.py install' command which is itself deprecated. pip 23.1 will enforce this behaviour change. A possible replacement is to use --config-settings. Discussion can be found at https://github.com/pypa/pip/issues/11358
DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
Collecting pyzmq==22.0.3
  Downloading pyzmq-22.0.3.tar.gz (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 10.6 MB/s eta 0:00:00
  Installing build dependencies ... error
  error: subprocess-exited-with-error
  
  × pip subprocess to install build dependencies did not run successfully.
  │ exit code: 1
  ╰─> [100 lines of output]
      DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
      Collecting setuptools>=40.8.0
        Downloading setuptools-65.5.0.tar.gz (2.6 MB)
           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 25.5 MB/s eta 0:00:00
        Getting requirements to build wheel: started
        Getting requirements to build wheel: finished with status 'done'
        Installing backend dependencies: started
        Installing backend dependencies: finished with status 'error'
        error: subprocess-exited-with-error
      
        × pip subprocess to install backend dependencies did not run successfully.
        │ exit code: 2
        ╰─> [77 lines of output]
            DEPRECATION: --no-binary currently disables reading from the cache of locally built wheels. In the future --no-binary will not influence the wheel cache. pip 23.1 will enforce this behaviour change. A possible replacement is to use the --no-cache-dir option. You can use the flag --use-feature=no-binary-enable-wheel-cache to test the upcoming behaviour. Discussion can be found at https://github.com/pypa/pip/issues/11453
            Collecting wheel
              Downloading wheel-0.38.0.tar.gz (65 kB)
                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.7/65.7 kB 1.3 MB/s eta 0:00:00
              Preparing metadata (setup.py): started
              Preparing metadata (setup.py): finished with status 'done'
            Collecting setuptools>=57.0.0
              Using cached setuptools-65.5.0.tar.gz (2.6 MB)
            ERROR: Exception:
            Traceback (most recent call last):
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/cli/base_command.py", line 160, in exc_logging_wrapper
                status = run_func(*args)
                         ^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/cli/req_command.py", line 247, in wrapper
                return func(self, options, args)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/commands/install.py", line 400, in run
                requirement_set = resolver.resolve(
                                  ^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 92, in resolve
                result = self._result = resolver.resolve(
                                        ^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py", line 481, in resolve
                state = resolution.resolve(requirements, max_rounds=max_rounds)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py", line 373, in resolve
                failure_causes = self._attempt_to_pin_criterion(name)
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py", line 213, in _attempt_to_pin_criterion
                criteria = self._get_updated_criteria(candidate)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py", line 204, in _get_updated_criteria
                self._add_to_criteria(criteria, requirement, parent=candidate)
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py", line 172, in _add_to_criteria
                if not criterion.candidates:
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_vendor/resolvelib/structs.py", line 151, in __bool__
                return bool(self._sequence)
                       ^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 155, in __bool__
                return any(self)
                       ^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 143, in <genexpr>
                return (c for c in iterator if id(c) not in self._incompatible_ids)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 47, in _iter_built
                candidate = func()
                            ^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 206, in _make_candidate_from_link
                self._link_candidate_cache[link] = LinkCandidate(
                                                   ^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 297, in __init__
                super().__init__(
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 162, in __init__
                self.dist = self._prepare()
                            ^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 231, in _prepare
                dist = self._prepare_distribution()
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 308, in _prepare_distribution
                return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/operations/prepare.py", line 491, in prepare_linked_requirement
                return self._prepare_linked_requirement(req, parallel_builds)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/operations/prepare.py", line 577, in _prepare_linked_requirement
                dist = _get_prepared_distribution(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/operations/prepare.py", line 68, in _get_prepared_distribution
                with build_tracker.track(req):
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 137, in __enter__
                return next(self.gen)
                       ^^^^^^^^^^^^^^
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/operations/build/build_tracker.py", line 122, in track
                self.add(req)
              File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pip/_internal/operations/build/build_tracker.py", line 92, in add
                raise LookupError(message)
            LookupError: https://files.pythonhosted.org/packages/c5/41/247814d8b7a044717164c74080725a6c8f3d2b5fc82b34bd825b617df663/setuptools-65.5.0.tar.gz (from https://pypi.org/simple/setuptools/) (requires-python:>=3.7) is already being built: setuptools>=40.8.0 from https://files.pythonhosted.org/packages/c5/41/247814d8b7a044717164c74080725a6c8f3d2b5fc82b34bd825b617df663/setuptools-65.5.0.tar.gz
            [end of output]
      
        note: This error originates from a subprocess, and is likely not a problem with pip.
      error: subprocess-exited-with-error
      
      × pip subprocess to install backend dependencies did not run successfully.
      │ exit code: 2
      ╰─> See above for output.
      
      note: This error originates from a subprocess, and is likely not a problem with pip.
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× pip subprocess to install build dependencies did not run successfully.
│ exit code: 1
╰─> See above for output.

note: This error originates from a subprocess, and is likely not a problem with pip.
```

The Internet tells me "is already being built" errors mean there is some sort of circular dependency. I suspect the change in https://github.com/pypa/wheel/compare/0.37.1..0.38.0#diff-fa602a8a75dc9dcc92261bac5f533c2a85e34fcceaff63b3a3a81d9acde2fc52 moving setuptools from setup_requires to install_requires is to blame.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pypa/wheel/pull/483?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) Report
Base: **66.85**% // Head: **67.18**% // Increases project coverage by **`+0.33%`** :tada:
> Coverage data is based on head [(`52f2d58`)](https://codecov.io/gh/pypa/wheel/pull/483?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) compared to base [(`7b9e8e1`)](https://codecov.io/gh/pypa/wheel/commit/7b9e8e1022b9c850756ef34bc1a4a88042988a86?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).
> Patch coverage: 50.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #483      +/-   ##
==========================================
+ Coverage   66.85%   67.18%   +0.33%     
==========================================
  Files          12       12              
  Lines         902      905       +3     
==========================================
+ Hits          603      608       +5     
+ Misses        299      297       -2     
```


| [Impacted Files](https://codecov.io/gh/pypa/wheel/pull/483?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) | Coverage Δ | |
|---|---|---|
| [src/wheel/cli/convert.py](https://codecov.io/gh/pypa/wheel/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL3doZWVsL2NsaS9jb252ZXJ0LnB5) | `38.60% <50.00%> (-0.11%)` | :arrow_down: |
| [src/wheel/metadata.py](https://codecov.io/gh/pypa/wheel/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL3doZWVsL21ldGFkYXRhLnB5) | `98.38% <0.00%> (+6.45%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/pypa/wheel/pull/483?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2a0fae9..b9db00a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
     args: ["-a", "from __future__ import annotations"]
 
 - repo: https://github.com/asottile/pyupgrade
-  rev: v3.1.0
+  rev: v3.2.0
   hooks:
   - id: pyupgrade
     args: ["--py37-plus"]
diff --git a/docs/news.rst b/docs/news.rst
index 8c4ce09..a64b5f1 100644
--- a/docs/news.rst
+++ b/docs/news.rst
@@ -1,6 +1,10 @@
 Release Notes
 =============
 
+**UNRELEASED**
+
+- Removed install dependency on setuptools
+
 **0.38.0 (2022-10-21)**
 
 - Dropped support for Python < 3.7
@@ -13,6 +17,8 @@ Release Notes
 - Fixed ``ValueError: ZIP does not support timestamps before 1980`` when using
   ``SOURCE_DATE_EPOCH=0`` or when on-disk timestamps are earlier than 1980-01-01. Such
   timestamps are now changed to the minimum value before packaging.
+- The future-proof fix in 0.36.0 for converting PyPy's SOABI into a abi tag was
+  faulty. Fixed so that future changes in the SOABI will not change the tag.
 
 **0.37.1 (2021-12-22)**
 
diff --git a/setup.cfg b/setup.cfg
index f83f17a..b4a7b14 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -32,7 +32,7 @@ package_dir=
     = src
 packages = find:
 python_requires = >=3.7
-install_requires = setuptools >= 57.0.0
+setup_requires = setuptools >= 45.2.0
 zip_safe = False
 
 [options.packages.find]
diff --git a/src/wheel/bdist_wheel.py b/src/wheel/bdist_wheel.py
index 7cbd140..4754fd1 100644
--- a/src/wheel/bdist_wheel.py
+++ b/src/wheel/bdist_wheel.py
@@ -15,9 +15,9 @@ import sysconfig
 import warnings
 from collections import OrderedDict
 from email.generator import BytesGenerator, Generator
+from glob import iglob
 from io import BytesIO
 from shutil import rmtree
-from sysconfig import get_config_var
 from zipfile import ZIP_DEFLATED, ZIP_STORED
 
 import pkg_resources
@@ -32,6 +32,9 @@ from .wheelfile import WheelFile
 
 safe_name = pkg_resources.safe_name
 safe_version = pkg_resources.safe_version
+setuptools_major_version = int(
+    pkg_resources.get_distribution("setuptools").version.split(".")[0]
+)
 
 PY_LIMITED_API_PATTERN = r"cp3\d"
 
@@ -55,7 +58,7 @@ def get_platform(archive_root):
 def get_flag(var, fallback, expected=True, warn=True):
     """Use a fallback value for determining SOABI flags if the needed config
     var is unset or unavailable."""
-    val = get_config_var(var)
+    val = sysconfig.get_config_var(var)
     if val is None:
         if warn:
             warnings.warn(
@@ -69,8 +72,8 @@ def get_flag(var, fallback, expected=True, warn=True):
 
 
 def get_abi_tag():
-    """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy)."""
-    soabi = get_config_var("SOABI")
+    """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
+    soabi = sysconfig.get_config_var("SOABI")
     impl = tags.interpreter_name()
     if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
         d = ""
@@ -87,9 +90,9 @@ def get_abi_tag():
             m = "m"
 
         abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
-    elif soabi and soabi.startswith("cpython-"):
+    elif soabi and impl == "cp":
         abi = "cp" + soabi.split("-")[1]
-    elif soabi and soabi.startswith("pypy-"):
+    elif soabi and impl == "pp":
         # we want something like pypy36-pp73
         abi = "-".join(soabi.split("-")[:2])
         abi = abi.replace(".", "_").replace("-", "_")
@@ -431,8 +434,47 @@ class bdist_wheel(Command):
 
     @property
     def license_paths(self):
-        metadata = self.distribution.metadata
-        return sorted(metadata.license_files or [])
+        if setuptools_major_version >= 57:
+            # Setuptools has resolved any patterns to actual file names
+            return self.distribution.metadata.license_files or ()
+
+        files = set()
+        metadata = self.distribution.get_option_dict("metadata")
+        if setuptools_major_version >= 42:
+            # Setuptools recognizes the license_files option but does not do globbing
+            patterns = self.distribution.metadata.license_files
+        else:
+            # Prior to those, wheel is entirely responsible for handling license files
+            if "license_files" in metadata:
+                patterns = metadata["license_files"][1].split()
+            else:
+                patterns = ()
+
+        if "license_file" in metadata:
+            warnings.warn(
+                'The "license_file" option is deprecated. Use "license_files" instead.',
+                DeprecationWarning,
+            )
+            files.add(metadata["license_file"][1])
+
+        if not files and not patterns and not isinstance(patterns, list):
+            patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
+
+        for pattern in patterns:
+            for path in iglob(pattern):
+                if path.endswith("~"):
+                    log.debug(
+                        f'ignoring license file "{path}" as it looks like a backup'
+                    )
+                    continue
+
+                if path not in files and os.path.isfile(path):
+                    log.info(
+                        f'adding license file "{path}" (matched pattern "{pattern}")'
+                    )
+                    files.add(path)
+
+        return files
 
     def egg2dist(self, egginfo_path, distinfo_path):
         """Convert an .egg-info directory into a .dist-info directory"""
diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py
index 0ca66f3..1287059 100755
--- a/src/wheel/cli/convert.py
+++ b/src/wheel/cli/convert.py
@@ -7,12 +7,15 @@ import tempfile
 import zipfile
 from glob import iglob
 
-from setuptools.dist import Distribution
-
 from ..bdist_wheel import bdist_wheel
 from ..wheelfile import WheelFile
 from . import WheelError
 
+try:
+    from setuptools import Distribution
+except ImportError:
+    from distutils.dist import Distribution
+
 egg_info_re = re.compile(
     r"""
     (?P<name>.+?)-(?P<ver>.+?)


Test patch:
diff --git a/tests/test_bdist_wheel.py b/tests/test_bdist_wheel.py
index 2a4d777..531d9e6 100644
--- a/tests/test_bdist_wheel.py
+++ b/tests/test_bdist_wheel.py
@@ -5,11 +5,13 @@ import shutil
 import stat
 import subprocess
 import sys
+import sysconfig
 from zipfile import ZipFile
 
 import pytest
 
-from wheel.bdist_wheel import bdist_wheel
+from wheel.bdist_wheel import bdist_wheel, get_abi_tag
+from wheel.vendored.packaging import tags
 from wheel.wheelfile import WheelFile
 
 DEFAULT_FILES = {
@@ -82,6 +84,17 @@ def test_licenses_default(dummy_dist, monkeypatch, tmpdir):
         assert set(wf.namelist()) == DEFAULT_FILES | license_files
 
 
+def test_licenses_deprecated(dummy_dist, monkeypatch, tmpdir):
+    dummy_dist.join("setup.cfg").write("[metadata]\nlicense_file=licenses/DUMMYFILE")
+    monkeypatch.chdir(dummy_dist)
+    subprocess.check_call(
+        [sys.executable, "setup.py", "bdist_wheel", "-b", str(tmpdir), "--universal"]
+    )
+    with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf:
+        license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"}
+        assert set(wf.namelist()) == DEFAULT_FILES | license_files
+
+
 @pytest.mark.parametrize(
     "config_file, config",
     [
@@ -218,3 +231,15 @@ def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmpdir):
             "--build-number=2",
         ]
     )
+
+
+def test_get_abi_tag_old(monkeypatch):
+    monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73")
+    assert get_abi_tag() == "pypy36_pp73"
+
+
+def test_get_abi_tag_new(monkeypatch):
+    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin")
+    monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+    assert get_abi_tag() == "pypy37_pp73"