From: Stefano Rivera <stefanor@debian.org>
Date: Sun, 16 Oct 2022 13:01:21 +0200
Subject: Search wheels for .dist-info directories

Some wheels don't use normalized names for their .dist-info directories,
so search the wheel for them.

Fixes: #134
Bug-Upstream: https://github.com/pypa/installer/issues/134
Bug-Debian: https://bugs.debian.org/1008606
Forwarded: https://github.com/pypa/installer/pull/137
---
 src/installer/sources.py | 27 +++++++++++++++++++++++++++
 src/installer/utils.py   |  8 ++++++++
 tests/test_sources.py    | 13 +++++++++++++
 tests/test_utils.py      | 22 ++++++++++++++++++++++
 4 files changed, 70 insertions(+)

diff --git a/src/installer/sources.py b/src/installer/sources.py
index 84c914f..76803e4 100644
--- a/src/installer/sources.py
+++ b/src/installer/sources.py
@@ -122,6 +122,33 @@ class WheelFile(WheelSource):
         with zipfile.ZipFile(path) as f:
             yield cls(f)
 
+    @property
+    def dist_info_dir(self) -> str:
+        """Name of the dist-info directory."""
+        if not hasattr(self, "_dist_info_dir"):
+            top_level_directories = {
+                path.split("/", 1)[0] for path in self._zipfile.namelist()
+            }
+            dist_infos = [
+                name for name in top_level_directories if name.endswith(".dist-info")
+            ]
+
+            assert (
+                len(dist_infos) == 1
+            ), "Wheel doesn't contain exactly one .dist-info directory"
+            dist_info_dir = dist_infos[0]
+
+            # NAME-VER.dist-info
+            di_dname = dist_info_dir.rsplit("-", 2)[0]
+            norm_di_dname = installer.utils.normalize_distribution_name(di_dname)
+            norm_file_dname = installer.utils.normalize_distribution_name(self.distribution)
+            assert (
+                norm_di_dname == norm_file_dname
+            ), "Wheel .dist-info directory doesn't match wheel filename"
+
+            self._dist_info_dir = dist_info_dir
+        return self._dist_info_dir
+
     @property
     def dist_info_filenames(self) -> List[str]:
         """Get names of all files in the dist-info directory."""
diff --git a/src/installer/utils.py b/src/installer/utils.py
index 0ccce12..efa4f66 100644
--- a/src/installer/utils.py
+++ b/src/installer/utils.py
@@ -92,6 +92,14 @@ def parse_metadata_file(contents: str) -> Message:
     return feed_parser.close()
 
 
+def normalize_distribution_name(name: str) -> str:
+    """Normalize a project name according to PEP-503.
+
+    :param name: The project name to normalize
+    """
+    return re.sub(r"[-_.]+", "-", name).lower()
+
+
 def parse_wheel_filename(filename: str) -> WheelFilename:
     """Parse a wheel filename, into it's various components.
 
diff --git a/tests/test_sources.py b/tests/test_sources.py
index a79cc24..e7cd092 100644
--- a/tests/test_sources.py
+++ b/tests/test_sources.py
@@ -92,3 +92,16 @@ class TestWheelFile:
 
         assert sorted(got_records) == sorted(expected_records)
         assert got_files == files
+
+    def test_finds_dist_info(self, fancy_wheel):
+        denorm = fancy_wheel.rename(fancy_wheel.parent / "Fancy-1.0.0-py3-none-any.whl")
+        with WheelFile.open(denorm) as source:
+            assert source.dist_info_filenames
+
+    def test_requires_dist_info_name_match(self, fancy_wheel):
+        misnamed = fancy_wheel.rename(
+            fancy_wheel.parent / "misnamed-1.0.0-py3-none-any.whl"
+        )
+        with pytest.raises(AssertionError):
+            with WheelFile.open(misnamed) as source:
+                source.dist_info_filenames
diff --git a/tests/test_utils.py b/tests/test_utils.py
index d5ba8c7..bf59962 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -15,6 +15,7 @@ from installer.utils import (
     construct_record_file,
     copyfileobj_with_hashing,
     fix_shebang,
+    normalize_distribution_name,
     parse_entrypoints,
     parse_metadata_file,
     parse_wheel_filename,
@@ -40,6 +41,27 @@ class TestParseMetadata:
         assert result.get_all("MULTI-USE-FIELD") == ["1", "2", "3"]
 
 
+class TestNormalizeDistributionName:
+    @pytest.mark.parametrize(
+        "string, expected",
+        [
+            # Noop
+            (
+                "package-1",
+                "package-1",
+            ),
+            # PEP 508 normalization
+            (
+                "ABC..12",
+                "abc-12",
+            ),
+        ],
+    )
+    def test_valid_cases(self, string, expected):
+        got = normalize_distribution_name(string)
+        assert expected == got, (expected, got)
+
+
 class TestParseWheelFilename:
     @pytest.mark.parametrize(
         "string, expected",
