We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[FR] Cache supported tags in Wheel.is_compatible
### What's the problem this feature will solve?

Calling `is_compatible` on an instance of the `Wheel` class takes mere milliseconds. But when calling this on thousands of wheels, this quickly adds up.

I think this method is called when setuptools or pip reads an index page, for example https://pypi.org/simple/setuptools/, and for each link checks if the wheel is compatible with the current interpreter and platform. (Not completely sure if this is how it works.)

My own use case is with Buildout. If this downloads a distribution, it saves it in a directory. Buildout uses this directory as an extra find-link. So the next time you call buildout, these distributions are available. This can help for the case where you have no internet, or someone has removed a distribution from PyPI (which happens a lot less these days, I am glad to say.) With thousands of wheels in there, and Buildout/setuptools calling `is_compatible` on each wheel, this takes too much time.

I created an [issue in Buildout](https://github.com/buildout/buildout/issues/626) to track this, so some more details are there. There it seems it is worse with the combination of the very latest setuptools (67.0.0) and pip (23.0.0), and extra worse on Python 3.8 compared to 3.11. But this is a bit unclear.

### Describe the solution you'd like

This is fixable in the `_is_compatible` method in [`setuptools/wheel.py`](https://github.com/pypa/setuptools/blob/v67.0.0/setuptools/wheel.py#L85-L89) by calculating the supported tags once, outside of the class.
When I checked on my system, this gives a set of 1700 supported tags. With 1000 wheels, we would calculate 1.7 million tags. A tad much. ;-)

The assumption is that calling `sys_tags` from the vendored packaging returns the same result each time.

I am preparing a PR.


### Alternative Solutions

I suppose Buildout could add another to its existing [patches](https://github.com/buildout/buildout/blob/master/src/zc/buildout/patches.py), which already includes `setuptools.package_index.PackageIndex` which is involved here.
But I think pip would benefit from a faster method as well.

### Additional context

Here is a test file to get some timings on your own system. Prerequisite: a directory with some wheel, the more the better.

```
from setuptools.wheel import Wheel
from time import time

import os

DIR = "/Users/maurits/cached-downloads/dist"
print(f"Looking for compatible wheels in {DIR}...")
wheels = 0
compatible = 0
start = time()
for filename in os.listdir(DIR):
    if not filename.endswith(".whl"):
        continue
    wheel = Wheel(os.path.join(DIR, filename))
    wheels += 1
    if wheel.is_compatible():
        compatible += 1
stop = time()
print(f"""
Processed {wheels} wheels.
There were {compatible} compatible wheels.
Time taken: {stop - start} seconds.
""")
```

Save this as `test.py`.
With a clone of the pip repo and using the main branch, this is the result:

```
$ .tox/python/bin/python test.py 
Looking for compatible wheels in /Users/maurits/cached-downloads/dist...

Processed 2284 wheels.
There were 1776 compatible wheels.
Time taken: 7.127894639968872 seconds.
```

With my branch I get the same numbers, except the time:

```
Time taken: 0.04627823829650879 seconds.
```

That is about 150 times faster.

### Code of Conduct

- [X] I agree to follow the PSF Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `setuptools/wheel.py`
Content:
```
1 """Wheels support."""
2 
3 import email
4 import itertools
5 import os
6 import posixpath
7 import re
8 import zipfile
9 import contextlib
10 
11 from distutils.util import get_platform
12 
13 import setuptools
14 from setuptools.extern.packaging.version import Version as parse_version
15 from setuptools.extern.packaging.tags import sys_tags
16 from setuptools.extern.packaging.utils import canonicalize_name
17 from setuptools.command.egg_info import write_requirements, _egg_basename
18 from setuptools.archive_util import _unpack_zipfile_obj
19 
20 
21 WHEEL_NAME = re.compile(
22     r"""^(?P<project_name>.+?)-(?P<version>\d.*?)
23     ((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)
24     )\.whl$""",
25     re.VERBOSE).match
26 
27 NAMESPACE_PACKAGE_INIT = \
28     "__import__('pkg_resources').declare_namespace(__name__)\n"
29 
30 
31 def unpack(src_dir, dst_dir):
32     '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
33     for dirpath, dirnames, filenames in os.walk(src_dir):
34         subdir = os.path.relpath(dirpath, src_dir)
35         for f in filenames:
36             src = os.path.join(dirpath, f)
37             dst = os.path.join(dst_dir, subdir, f)
38             os.renames(src, dst)
39         for n, d in reversed(list(enumerate(dirnames))):
40             src = os.path.join(dirpath, d)
41             dst = os.path.join(dst_dir, subdir, d)
42             if not os.path.exists(dst):
43                 # Directory does not exist in destination,
44                 # rename it and prune it from os.walk list.
45                 os.renames(src, dst)
46                 del dirnames[n]
47     # Cleanup.
48     for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):
49         assert not filenames
50         os.rmdir(dirpath)
51 
52 
53 @contextlib.contextmanager
54 def disable_info_traces():
55     """
56     Temporarily disable info traces.
57     """
58     from distutils import log
59     saved = log.set_threshold(log.WARN)
60     try:
61         yield
62     finally:
63         log.set_threshold(saved)
64 
65 
66 class Wheel:
67 
68     def __init__(self, filename):
69         match = WHEEL_NAME(os.path.basename(filename))
70         if match is None:
71             raise ValueError('invalid wheel name: %r' % filename)
72         self.filename = filename
73         for k, v in match.groupdict().items():
74             setattr(self, k, v)
75 
76     def tags(self):
77         '''List tags (py_version, abi, platform) supported by this wheel.'''
78         return itertools.product(
79             self.py_version.split('.'),
80             self.abi.split('.'),
81             self.platform.split('.'),
82         )
83 
84     def is_compatible(self):
85         '''Is the wheel is compatible with the current platform?'''
86         supported_tags = set(
87             (t.interpreter, t.abi, t.platform) for t in sys_tags())
88         return next((True for t in self.tags() if t in supported_tags), False)
89 
90     def egg_name(self):
91         return _egg_basename(
92             self.project_name,
93             self.version,
94             platform=(None if self.platform == 'any' else get_platform()),
95         ) + ".egg"
96 
97     def get_dist_info(self, zf):
98         # find the correct name of the .dist-info dir in the wheel file
99         for member in zf.namelist():
100             dirname = posixpath.dirname(member)
101             if (dirname.endswith('.dist-info') and
102                     canonicalize_name(dirname).startswith(
103                         canonicalize_name(self.project_name))):
104                 return dirname
105         raise ValueError("unsupported wheel format. .dist-info not found")
106 
107     def install_as_egg(self, destination_eggdir):
108         '''Install wheel as an egg directory.'''
109         with zipfile.ZipFile(self.filename) as zf:
110             self._install_as_egg(destination_eggdir, zf)
111 
112     def _install_as_egg(self, destination_eggdir, zf):
113         dist_basename = '%s-%s' % (self.project_name, self.version)
114         dist_info = self.get_dist_info(zf)
115         dist_data = '%s.data' % dist_basename
116         egg_info = os.path.join(destination_eggdir, 'EGG-INFO')
117 
118         self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)
119         self._move_data_entries(destination_eggdir, dist_data)
120         self._fix_namespace_packages(egg_info, destination_eggdir)
121 
122     @staticmethod
123     def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):
124         import pkg_resources
125 
126         def get_metadata(name):
127             with zf.open(posixpath.join(dist_info, name)) as fp:
128                 value = fp.read().decode('utf-8')
129                 return email.parser.Parser().parsestr(value)
130 
131         wheel_metadata = get_metadata('WHEEL')
132         # Check wheel format version is supported.
133         wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))
134         wheel_v1 = (
135             parse_version('1.0') <= wheel_version < parse_version('2.0dev0')
136         )
137         if not wheel_v1:
138             raise ValueError(
139                 'unsupported wheel format version: %s' % wheel_version)
140         # Extract to target directory.
141         _unpack_zipfile_obj(zf, destination_eggdir)
142         # Convert metadata.
143         dist_info = os.path.join(destination_eggdir, dist_info)
144         dist = pkg_resources.Distribution.from_location(
145             destination_eggdir, dist_info,
146             metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
147         )
148 
149         # Note: Evaluate and strip markers now,
150         # as it's difficult to convert back from the syntax:
151         # foobar; "linux" in sys_platform and extra == 'test'
152         def raw_req(req):
153             req.marker = None
154             return str(req)
155         install_requires = list(map(raw_req, dist.requires()))
156         extras_require = {
157             extra: [
158                 req
159                 for req in map(raw_req, dist.requires((extra,)))
160                 if req not in install_requires
161             ]
162             for extra in dist.extras
163         }
164         os.rename(dist_info, egg_info)
165         os.rename(
166             os.path.join(egg_info, 'METADATA'),
167             os.path.join(egg_info, 'PKG-INFO'),
168         )
169         setup_dist = setuptools.Distribution(
170             attrs=dict(
171                 install_requires=install_requires,
172                 extras_require=extras_require,
173             ),
174         )
175         with disable_info_traces():
176             write_requirements(
177                 setup_dist.get_command_obj('egg_info'),
178                 None,
179                 os.path.join(egg_info, 'requires.txt'),
180             )
181 
182     @staticmethod
183     def _move_data_entries(destination_eggdir, dist_data):
184         """Move data entries to their correct location."""
185         dist_data = os.path.join(destination_eggdir, dist_data)
186         dist_data_scripts = os.path.join(dist_data, 'scripts')
187         if os.path.exists(dist_data_scripts):
188             egg_info_scripts = os.path.join(
189                 destination_eggdir, 'EGG-INFO', 'scripts')
190             os.mkdir(egg_info_scripts)
191             for entry in os.listdir(dist_data_scripts):
192                 # Remove bytecode, as it's not properly handled
193                 # during easy_install scripts install phase.
194                 if entry.endswith('.pyc'):
195                     os.unlink(os.path.join(dist_data_scripts, entry))
196                 else:
197                     os.rename(
198                         os.path.join(dist_data_scripts, entry),
199                         os.path.join(egg_info_scripts, entry),
200                     )
201             os.rmdir(dist_data_scripts)
202         for subdir in filter(os.path.exists, (
203             os.path.join(dist_data, d)
204             for d in ('data', 'headers', 'purelib', 'platlib')
205         )):
206             unpack(subdir, destination_eggdir)
207         if os.path.exists(dist_data):
208             os.rmdir(dist_data)
209 
210     @staticmethod
211     def _fix_namespace_packages(egg_info, destination_eggdir):
212         namespace_packages = os.path.join(
213             egg_info, 'namespace_packages.txt')
214         if os.path.exists(namespace_packages):
215             with open(namespace_packages) as fp:
216                 namespace_packages = fp.read().split()
217             for mod in namespace_packages:
218                 mod_dir = os.path.join(destination_eggdir, *mod.split('.'))
219                 mod_init = os.path.join(mod_dir, '__init__.py')
220                 if not os.path.exists(mod_dir):
221                     os.mkdir(mod_dir)
222                 if not os.path.exists(mod_init):
223                     with open(mod_init, 'w') as fp:
224                         fp.write(NAMESPACE_PACKAGE_INIT)
225
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/setuptools/wheel.py b/setuptools/wheel.py
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -2,6 +2,7 @@
 
 import email
 import itertools
+import functools
 import os
 import posixpath
 import re
@@ -28,6 +29,14 @@
     "__import__('pkg_resources').declare_namespace(__name__)\n"
 
 
+@functools.lru_cache(maxsize=None)
+def _get_supported_tags():
+    # We calculate the supported tags only once, otherwise calling
+    # this method on thousands of wheels takes seconds instead of
+    # milliseconds.
+    return {(t.interpreter, t.abi, t.platform) for t in sys_tags()}
+
+
 def unpack(src_dir, dst_dir):
     '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
     for dirpath, dirnames, filenames in os.walk(src_dir):
@@ -82,10 +91,8 @@
         )
 
     def is_compatible(self):
-        '''Is the wheel is compatible with the current platform?'''
-        supported_tags = set(
-            (t.interpreter, t.abi, t.platform) for t in sys_tags())
-        return next((True for t in self.tags() if t in supported_tags), False)
+        '''Is the wheel compatible with the current platform?'''
+        return next((True for t in self.tags() if t in _get_supported_tags()), False)
 
     def egg_name(self):
         return _egg_basename(

{"golden_diff": "diff --git a/setuptools/wheel.py b/setuptools/wheel.py\n--- a/setuptools/wheel.py\n+++ b/setuptools/wheel.py\n@@ -2,6 +2,7 @@\n \n import email\n import itertools\n+import functools\n import os\n import posixpath\n import re\n@@ -28,6 +29,14 @@\n     \"__import__('pkg_resources').declare_namespace(__name__)\\n\"\n \n \n+@functools.lru_cache(maxsize=None)\n+def _get_supported_tags():\n+    # We calculate the supported tags only once, otherwise calling\n+    # this method on thousands of wheels takes seconds instead of\n+    # milliseconds.\n+    return {(t.interpreter, t.abi, t.platform) for t in sys_tags()}\n+\n+\n def unpack(src_dir, dst_dir):\n     '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''\n     for dirpath, dirnames, filenames in os.walk(src_dir):\n@@ -82,10 +91,8 @@\n         )\n \n     def is_compatible(self):\n-        '''Is the wheel is compatible with the current platform?'''\n-        supported_tags = set(\n-            (t.interpreter, t.abi, t.platform) for t in sys_tags())\n-        return next((True for t in self.tags() if t in supported_tags), False)\n+        '''Is the wheel compatible with the current platform?'''\n+        return next((True for t in self.tags() if t in _get_supported_tags()), False)\n \n     def egg_name(self):\n         return _egg_basename(\n", "issue": "[FR] Cache supported tags in Wheel.is_compatible\n### What's the problem this feature will solve?\n\nCalling `is_compatible` on an instance of the `Wheel` class takes mere milliseconds. But when calling this on thousands of wheels, this quickly adds up.\r\n\r\nI think this method is called when setuptools or pip reads an index page, for example https://pypi.org/simple/setuptools/, and for each link checks if the wheel is compatible with the current interpreter and platform. (Not completely sure if this is how it works.)\r\n\r\nMy own use case is with Buildout. If this downloads a distribution, it saves it in a directory. Buildout uses this directory as an extra find-link. So the next time you call buildout, these distributions are available. This can help for the case where you have no internet, or someone has removed a distribution from PyPI (which happens a lot less these days, I am glad to say.) With thousands of wheels in there, and Buildout/setuptools calling `is_compatible` on each wheel, this takes too much time.\r\n\r\nI created an [issue in Buildout](https://github.com/buildout/buildout/issues/626) to track this, so some more details are there. There it seems it is worse with the combination of the very latest setuptools (67.0.0) and pip (23.0.0), and extra worse on Python 3.8 compared to 3.11. But this is a bit unclear.\n\n### Describe the solution you'd like\n\nThis is fixable in the `_is_compatible` method in [`setuptools/wheel.py`](https://github.com/pypa/setuptools/blob/v67.0.0/setuptools/wheel.py#L85-L89) by calculating the supported tags once, outside of the class.\r\nWhen I checked on my system, this gives a set of 1700 supported tags. With 1000 wheels, we would calculate 1.7 million tags. A tad much. ;-)\r\n\r\nThe assumption is that calling `sys_tags` from the vendored packaging returns the same result each time.\r\n\r\nI am preparing a PR.\r\n\n\n### Alternative Solutions\n\nI suppose Buildout could add another to its existing [patches](https://github.com/buildout/buildout/blob/master/src/zc/buildout/patches.py), which already includes `setuptools.package_index.PackageIndex` which is involved here.\r\nBut I think pip would benefit from a faster method as well.\n\n### Additional context\n\nHere is a test file to get some timings on your own system. Prerequisite: a directory with some wheel, the more the better.\r\n\r\n```\r\nfrom setuptools.wheel import Wheel\r\nfrom time import time\r\n\r\nimport os\r\n\r\nDIR = \"/Users/maurits/cached-downloads/dist\"\r\nprint(f\"Looking for compatible wheels in {DIR}...\")\r\nwheels = 0\r\ncompatible = 0\r\nstart = time()\r\nfor filename in os.listdir(DIR):\r\n    if not filename.endswith(\".whl\"):\r\n        continue\r\n    wheel = Wheel(os.path.join(DIR, filename))\r\n    wheels += 1\r\n    if wheel.is_compatible():\r\n        compatible += 1\r\nstop = time()\r\nprint(f\"\"\"\r\nProcessed {wheels} wheels.\r\nThere were {compatible} compatible wheels.\r\nTime taken: {stop - start} seconds.\r\n\"\"\")\r\n```\r\n\r\nSave this as `test.py`.\r\nWith a clone of the pip repo and using the main branch, this is the result:\r\n\r\n```\r\n$ .tox/python/bin/python test.py \r\nLooking for compatible wheels in /Users/maurits/cached-downloads/dist...\r\n\r\nProcessed 2284 wheels.\r\nThere were 1776 compatible wheels.\r\nTime taken: 7.127894639968872 seconds.\r\n```\r\n\r\nWith my branch I get the same numbers, except the time:\r\n\r\n```\r\nTime taken: 0.04627823829650879 seconds.\r\n```\r\n\r\nThat is about 150 times faster.\n\n### Code of Conduct\n\n- [X] I agree to follow the PSF Code of Conduct\n", "before_files": [{"content": "\"\"\"Wheels support.\"\"\"\n\nimport email\nimport itertools\nimport os\nimport posixpath\nimport re\nimport zipfile\nimport contextlib\n\nfrom distutils.util import get_platform\n\nimport setuptools\nfrom setuptools.extern.packaging.version import Version as parse_version\nfrom setuptools.extern.packaging.tags import sys_tags\nfrom setuptools.extern.packaging.utils import canonicalize_name\nfrom setuptools.command.egg_info import write_requirements, _egg_basename\nfrom setuptools.archive_util import _unpack_zipfile_obj\n\n\nWHEEL_NAME = re.compile(\n    r\"\"\"^(?P<project_name>.+?)-(?P<version>\\d.*?)\n    ((-(?P<build>\\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)\n    )\\.whl$\"\"\",\n    re.VERBOSE).match\n\nNAMESPACE_PACKAGE_INIT = \\\n    \"__import__('pkg_resources').declare_namespace(__name__)\\n\"\n\n\ndef unpack(src_dir, dst_dir):\n    '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''\n    for dirpath, dirnames, filenames in os.walk(src_dir):\n        subdir = os.path.relpath(dirpath, src_dir)\n        for f in filenames:\n            src = os.path.join(dirpath, f)\n            dst = os.path.join(dst_dir, subdir, f)\n            os.renames(src, dst)\n        for n, d in reversed(list(enumerate(dirnames))):\n            src = os.path.join(dirpath, d)\n            dst = os.path.join(dst_dir, subdir, d)\n            if not os.path.exists(dst):\n                # Directory does not exist in destination,\n                # rename it and prune it from os.walk list.\n                os.renames(src, dst)\n                del dirnames[n]\n    # Cleanup.\n    for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):\n        assert not filenames\n        os.rmdir(dirpath)\n\n\n@contextlib.contextmanager\ndef disable_info_traces():\n    \"\"\"\n    Temporarily disable info traces.\n    \"\"\"\n    from distutils import log\n    saved = log.set_threshold(log.WARN)\n    try:\n        yield\n    finally:\n        log.set_threshold(saved)\n\n\nclass Wheel:\n\n    def __init__(self, filename):\n        match = WHEEL_NAME(os.path.basename(filename))\n        if match is None:\n            raise ValueError('invalid wheel name: %r' % filename)\n        self.filename = filename\n        for k, v in match.groupdict().items():\n            setattr(self, k, v)\n\n    def tags(self):\n        '''List tags (py_version, abi, platform) supported by this wheel.'''\n        return itertools.product(\n            self.py_version.split('.'),\n            self.abi.split('.'),\n            self.platform.split('.'),\n        )\n\n    def is_compatible(self):\n        '''Is the wheel is compatible with the current platform?'''\n        supported_tags = set(\n            (t.interpreter, t.abi, t.platform) for t in sys_tags())\n        return next((True for t in self.tags() if t in supported_tags), False)\n\n    def egg_name(self):\n        return _egg_basename(\n            self.project_name,\n            self.version,\n            platform=(None if self.platform == 'any' else get_platform()),\n        ) + \".egg\"\n\n    def get_dist_info(self, zf):\n        # find the correct name of the .dist-info dir in the wheel file\n        for member in zf.namelist():\n            dirname = posixpath.dirname(member)\n            if (dirname.endswith('.dist-info') and\n                    canonicalize_name(dirname).startswith(\n                        canonicalize_name(self.project_name))):\n                return dirname\n        raise ValueError(\"unsupported wheel format. .dist-info not found\")\n\n    def install_as_egg(self, destination_eggdir):\n        '''Install wheel as an egg directory.'''\n        with zipfile.ZipFile(self.filename) as zf:\n            self._install_as_egg(destination_eggdir, zf)\n\n    def _install_as_egg(self, destination_eggdir, zf):\n        dist_basename = '%s-%s' % (self.project_name, self.version)\n        dist_info = self.get_dist_info(zf)\n        dist_data = '%s.data' % dist_basename\n        egg_info = os.path.join(destination_eggdir, 'EGG-INFO')\n\n        self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)\n        self._move_data_entries(destination_eggdir, dist_data)\n        self._fix_namespace_packages(egg_info, destination_eggdir)\n\n    @staticmethod\n    def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):\n        import pkg_resources\n\n        def get_metadata(name):\n            with zf.open(posixpath.join(dist_info, name)) as fp:\n                value = fp.read().decode('utf-8')\n                return email.parser.Parser().parsestr(value)\n\n        wheel_metadata = get_metadata('WHEEL')\n        # Check wheel format version is supported.\n        wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))\n        wheel_v1 = (\n            parse_version('1.0') <= wheel_version < parse_version('2.0dev0')\n        )\n        if not wheel_v1:\n            raise ValueError(\n                'unsupported wheel format version: %s' % wheel_version)\n        # Extract to target directory.\n        _unpack_zipfile_obj(zf, destination_eggdir)\n        # Convert metadata.\n        dist_info = os.path.join(destination_eggdir, dist_info)\n        dist = pkg_resources.Distribution.from_location(\n            destination_eggdir, dist_info,\n            metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),\n        )\n\n        # Note: Evaluate and strip markers now,\n        # as it's difficult to convert back from the syntax:\n        # foobar; \"linux\" in sys_platform and extra == 'test'\n        def raw_req(req):\n            req.marker = None\n            return str(req)\n        install_requires = list(map(raw_req, dist.requires()))\n        extras_require = {\n            extra: [\n                req\n                for req in map(raw_req, dist.requires((extra,)))\n                if req not in install_requires\n            ]\n            for extra in dist.extras\n        }\n        os.rename(dist_info, egg_info)\n        os.rename(\n            os.path.join(egg_info, 'METADATA'),\n            os.path.join(egg_info, 'PKG-INFO'),\n        )\n        setup_dist = setuptools.Distribution(\n            attrs=dict(\n                install_requires=install_requires,\n                extras_require=extras_require,\n            ),\n        )\n        with disable_info_traces():\n            write_requirements(\n                setup_dist.get_command_obj('egg_info'),\n                None,\n                os.path.join(egg_info, 'requires.txt'),\n            )\n\n    @staticmethod\n    def _move_data_entries(destination_eggdir, dist_data):\n        \"\"\"Move data entries to their correct location.\"\"\"\n        dist_data = os.path.join(destination_eggdir, dist_data)\n        dist_data_scripts = os.path.join(dist_data, 'scripts')\n        if os.path.exists(dist_data_scripts):\n            egg_info_scripts = os.path.join(\n                destination_eggdir, 'EGG-INFO', 'scripts')\n            os.mkdir(egg_info_scripts)\n            for entry in os.listdir(dist_data_scripts):\n                # Remove bytecode, as it's not properly handled\n                # during easy_install scripts install phase.\n                if entry.endswith('.pyc'):\n                    os.unlink(os.path.join(dist_data_scripts, entry))\n                else:\n                    os.rename(\n                        os.path.join(dist_data_scripts, entry),\n                        os.path.join(egg_info_scripts, entry),\n                    )\n            os.rmdir(dist_data_scripts)\n        for subdir in filter(os.path.exists, (\n            os.path.join(dist_data, d)\n            for d in ('data', 'headers', 'purelib', 'platlib')\n        )):\n            unpack(subdir, destination_eggdir)\n        if os.path.exists(dist_data):\n            os.rmdir(dist_data)\n\n    @staticmethod\n    def _fix_namespace_packages(egg_info, destination_eggdir):\n        namespace_packages = os.path.join(\n            egg_info, 'namespace_packages.txt')\n        if os.path.exists(namespace_packages):\n            with open(namespace_packages) as fp:\n                namespace_packages = fp.read().split()\n            for mod in namespace_packages:\n                mod_dir = os.path.join(destination_eggdir, *mod.split('.'))\n                mod_init = os.path.join(mod_dir, '__init__.py')\n                if not os.path.exists(mod_dir):\n                    os.mkdir(mod_dir)\n                if not os.path.exists(mod_init):\n                    with open(mod_init, 'w') as fp:\n                        fp.write(NAMESPACE_PACKAGE_INIT)\n", "path": "setuptools/wheel.py"}], "after_files": [{"content": "\"\"\"Wheels support.\"\"\"\n\nimport email\nimport itertools\nimport functools\nimport os\nimport posixpath\nimport re\nimport zipfile\nimport contextlib\n\nfrom distutils.util import get_platform\n\nimport setuptools\nfrom setuptools.extern.packaging.version import Version as parse_version\nfrom setuptools.extern.packaging.tags import sys_tags\nfrom setuptools.extern.packaging.utils import canonicalize_name\nfrom setuptools.command.egg_info import write_requirements, _egg_basename\nfrom setuptools.archive_util import _unpack_zipfile_obj\n\n\nWHEEL_NAME = re.compile(\n    r\"\"\"^(?P<project_name>.+?)-(?P<version>\\d.*?)\n    ((-(?P<build>\\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)\n    )\\.whl$\"\"\",\n    re.VERBOSE).match\n\nNAMESPACE_PACKAGE_INIT = \\\n    \"__import__('pkg_resources').declare_namespace(__name__)\\n\"\n\n\n@functools.lru_cache(maxsize=None)\ndef _get_supported_tags():\n    # We calculate the supported tags only once, otherwise calling\n    # this method on thousands of wheels takes seconds instead of\n    # milliseconds.\n    return {(t.interpreter, t.abi, t.platform) for t in sys_tags()}\n\n\ndef unpack(src_dir, dst_dir):\n    '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''\n    for dirpath, dirnames, filenames in os.walk(src_dir):\n        subdir = os.path.relpath(dirpath, src_dir)\n        for f in filenames:\n            src = os.path.join(dirpath, f)\n            dst = os.path.join(dst_dir, subdir, f)\n            os.renames(src, dst)\n        for n, d in reversed(list(enumerate(dirnames))):\n            src = os.path.join(dirpath, d)\n            dst = os.path.join(dst_dir, subdir, d)\n            if not os.path.exists(dst):\n                # Directory does not exist in destination,\n                # rename it and prune it from os.walk list.\n                os.renames(src, dst)\n                del dirnames[n]\n    # Cleanup.\n    for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):\n        assert not filenames\n        os.rmdir(dirpath)\n\n\n@contextlib.contextmanager\ndef disable_info_traces():\n    \"\"\"\n    Temporarily disable info traces.\n    \"\"\"\n    from distutils import log\n    saved = log.set_threshold(log.WARN)\n    try:\n        yield\n    finally:\n        log.set_threshold(saved)\n\n\nclass Wheel:\n\n    def __init__(self, filename):\n        match = WHEEL_NAME(os.path.basename(filename))\n        if match is None:\n            raise ValueError('invalid wheel name: %r' % filename)\n        self.filename = filename\n        for k, v in match.groupdict().items():\n            setattr(self, k, v)\n\n    def tags(self):\n        '''List tags (py_version, abi, platform) supported by this wheel.'''\n        return itertools.product(\n            self.py_version.split('.'),\n            self.abi.split('.'),\n            self.platform.split('.'),\n        )\n\n    def is_compatible(self):\n        '''Is the wheel compatible with the current platform?'''\n        return next((True for t in self.tags() if t in _get_supported_tags()), False)\n\n    def egg_name(self):\n        return _egg_basename(\n            self.project_name,\n            self.version,\n            platform=(None if self.platform == 'any' else get_platform()),\n        ) + \".egg\"\n\n    def get_dist_info(self, zf):\n        # find the correct name of the .dist-info dir in the wheel file\n        for member in zf.namelist():\n            dirname = posixpath.dirname(member)\n            if (dirname.endswith('.dist-info') and\n                    canonicalize_name(dirname).startswith(\n                        canonicalize_name(self.project_name))):\n                return dirname\n        raise ValueError(\"unsupported wheel format. .dist-info not found\")\n\n    def install_as_egg(self, destination_eggdir):\n        '''Install wheel as an egg directory.'''\n        with zipfile.ZipFile(self.filename) as zf:\n            self._install_as_egg(destination_eggdir, zf)\n\n    def _install_as_egg(self, destination_eggdir, zf):\n        dist_basename = '%s-%s' % (self.project_name, self.version)\n        dist_info = self.get_dist_info(zf)\n        dist_data = '%s.data' % dist_basename\n        egg_info = os.path.join(destination_eggdir, 'EGG-INFO')\n\n        self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)\n        self._move_data_entries(destination_eggdir, dist_data)\n        self._fix_namespace_packages(egg_info, destination_eggdir)\n\n    @staticmethod\n    def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):\n        import pkg_resources\n\n        def get_metadata(name):\n            with zf.open(posixpath.join(dist_info, name)) as fp:\n                value = fp.read().decode('utf-8')\n                return email.parser.Parser().parsestr(value)\n\n        wheel_metadata = get_metadata('WHEEL')\n        # Check wheel format version is supported.\n        wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))\n        wheel_v1 = (\n            parse_version('1.0') <= wheel_version < parse_version('2.0dev0')\n        )\n        if not wheel_v1:\n            raise ValueError(\n                'unsupported wheel format version: %s' % wheel_version)\n        # Extract to target directory.\n        _unpack_zipfile_obj(zf, destination_eggdir)\n        # Convert metadata.\n        dist_info = os.path.join(destination_eggdir, dist_info)\n        dist = pkg_resources.Distribution.from_location(\n            destination_eggdir, dist_info,\n            metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),\n        )\n\n        # Note: Evaluate and strip markers now,\n        # as it's difficult to convert back from the syntax:\n        # foobar; \"linux\" in sys_platform and extra == 'test'\n        def raw_req(req):\n            req.marker = None\n            return str(req)\n        install_requires = list(map(raw_req, dist.requires()))\n        extras_require = {\n            extra: [\n                req\n                for req in map(raw_req, dist.requires((extra,)))\n                if req not in install_requires\n            ]\n            for extra in dist.extras\n        }\n        os.rename(dist_info, egg_info)\n        os.rename(\n            os.path.join(egg_info, 'METADATA'),\n            os.path.join(egg_info, 'PKG-INFO'),\n        )\n        setup_dist = setuptools.Distribution(\n            attrs=dict(\n                install_requires=install_requires,\n                extras_require=extras_require,\n            ),\n        )\n        with disable_info_traces():\n            write_requirements(\n                setup_dist.get_command_obj('egg_info'),\n                None,\n                os.path.join(egg_info, 'requires.txt'),\n            )\n\n    @staticmethod\n    def _move_data_entries(destination_eggdir, dist_data):\n        \"\"\"Move data entries to their correct location.\"\"\"\n        dist_data = os.path.join(destination_eggdir, dist_data)\n        dist_data_scripts = os.path.join(dist_data, 'scripts')\n        if os.path.exists(dist_data_scripts):\n            egg_info_scripts = os.path.join(\n                destination_eggdir, 'EGG-INFO', 'scripts')\n            os.mkdir(egg_info_scripts)\n            for entry in os.listdir(dist_data_scripts):\n                # Remove bytecode, as it's not properly handled\n                # during easy_install scripts install phase.\n                if entry.endswith('.pyc'):\n                    os.unlink(os.path.join(dist_data_scripts, entry))\n                else:\n                    os.rename(\n                        os.path.join(dist_data_scripts, entry),\n                        os.path.join(egg_info_scripts, entry),\n                    )\n            os.rmdir(dist_data_scripts)\n        for subdir in filter(os.path.exists, (\n            os.path.join(dist_data, d)\n            for d in ('data', 'headers', 'purelib', 'platlib')\n        )):\n            unpack(subdir, destination_eggdir)\n        if os.path.exists(dist_data):\n            os.rmdir(dist_data)\n\n    @staticmethod\n    def _fix_namespace_packages(egg_info, destination_eggdir):\n        namespace_packages = os.path.join(\n            egg_info, 'namespace_packages.txt')\n        if os.path.exists(namespace_packages):\n            with open(namespace_packages) as fp:\n                namespace_packages = fp.read().split()\n            for mod in namespace_packages:\n                mod_dir = os.path.join(destination_eggdir, *mod.split('.'))\n                mod_init = os.path.join(mod_dir, '__init__.py')\n                if not os.path.exists(mod_dir):\n                    os.mkdir(mod_dir)\n                if not os.path.exists(mod_init):\n                    with open(mod_init, 'w') as fp:\n                        fp.write(NAMESPACE_PACKAGE_INIT)\n", "path": "setuptools/wheel.py"}]}