From fd6d2ae1177cf925c929af1decad9af66653daec Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Tue, 16 Aug 2022 22:18:15 +0200 Subject: [PATCH 1/5] Search weels for .dist-info directories Some wheels don't use canonicalized names for their .dist-info directories, so search the wheel for them. Fixes: #134 --- src/installer/sources.py | 16 ++++++++++++++++ tests/test_sources.py | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/installer/sources.py b/src/installer/sources.py index 8bddcfce..9d491f4f 100644 --- a/src/installer/sources.py +++ b/src/installer/sources.py @@ -145,6 +145,22 @@ def open(cls, path: "os.PathLike[str]") -> Iterator["WheelFile"]: 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" + self._dist_info_dir = dist_infos[0] + 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/tests/test_sources.py b/tests/test_sources.py index 4ae4fb47..2db49a26 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -99,6 +99,11 @@ def test_provides_correct_contents(self, fancy_wheel): 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 replace_file_in_zip(path: str, filename: str, content: "bytes | None") -> None: """Helper function for replacing a file in the zip. From ed2426ec6c78845ed742ec4cf4069d41f4df4d59 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Mon, 15 Aug 2022 12:11:33 +0200 Subject: [PATCH 2/5] Add a PEP 503 distribution name canonicalization method to utils --- src/installer/utils.py | 8 ++++++++ tests/test_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/installer/utils.py b/src/installer/utils.py index 7b1404d0..cef2bd8e 100644 --- a/src/installer/utils.py +++ b/src/installer/utils.py @@ -94,6 +94,14 @@ def parse_metadata_file(contents: str) -> Message: return feed_parser.close() +def canonicalize_name(name: str) -> str: + """Canonicalize a project name according to PEP-503. + + :param name: The project name to canonicalize + """ + 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_utils.py b/tests/test_utils.py index bfcc0899..e4bfb6a3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,6 +16,7 @@ construct_record_file, copyfileobj_with_hashing, fix_shebang, + canonicalize_name, parse_entrypoints, parse_metadata_file, parse_wheel_filename, @@ -41,6 +42,27 @@ def test_basics(self): assert result.get_all("MULTI-USE-FIELD") == ["1", "2", "3"] +class TestCanonicalizeDistributionName: + @pytest.mark.parametrize( + "string, expected", + [ + # Noop + ( + "package-1", + "package-1", + ), + # PEP 508 canonicalization + ( + "ABC..12", + "abc-12", + ), + ], + ) + def test_valid_cases(self, string, expected): + got = canonicalize_name(string) + assert expected == got, (expected, got) + + class TestParseWheelFilename: @pytest.mark.parametrize( "string, expected", From 142e3338388cc6e68f4b33b7a4ba19c778e91824 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Tue, 16 Aug 2022 23:12:24 +0200 Subject: [PATCH 3/5] Require .dist-info directory name to match filename At least, once they are both normalized. --- src/installer/sources.py | 15 +++++++++++++-- tests/test_sources.py | 8 ++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/installer/sources.py b/src/installer/sources.py index 9d491f4f..0d70f103 100644 --- a/src/installer/sources.py +++ b/src/installer/sources.py @@ -8,7 +8,7 @@ from typing import BinaryIO, ClassVar, Iterator, List, Tuple, Type, cast from installer.records import RecordEntry, parse_record_file -from installer.utils import parse_wheel_filename +from installer.utils import canonicalize_name, parse_wheel_filename WheelContentElement = Tuple[Tuple[str, str, str], BinaryIO, bool] @@ -155,10 +155,21 @@ def dist_info_dir(self) -> str: 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" - self._dist_info_dir = dist_infos[0] + dist_info_dir = dist_infos[0] + + # NAME-VER.dist-info + di_dname = dist_info_dir.rsplit("-", 2)[0] + norm_di_dname = canonicalize_name(di_dname) + norm_file_dname = canonicalize_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 diff --git a/tests/test_sources.py b/tests/test_sources.py index 2db49a26..e29ba26a 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -104,6 +104,14 @@ def test_finds_dist_info(self, fancy_wheel): 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 + def replace_file_in_zip(path: str, filename: str, content: "bytes | None") -> None: """Helper function for replacing a file in the zip. From 49e86a40ff6d965d725e1e18299d12d8a6b2a6c9 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Mon, 12 Dec 2022 16:34:12 -0400 Subject: [PATCH 4/5] Add Python 3.7 support to new tests --- tests/test_sources.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_sources.py b/tests/test_sources.py index e29ba26a..ec71351f 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -101,6 +101,8 @@ def test_provides_correct_contents(self, fancy_wheel): def test_finds_dist_info(self, fancy_wheel): denorm = fancy_wheel.rename(fancy_wheel.parent / "Fancy-1.0.0-py3-none-any.whl") + # Python 3.7: rename doesn't return the new name: + denorm = fancy_wheel.parent / "Fancy-1.0.0-py3-none-any.whl" with WheelFile.open(denorm) as source: assert source.dist_info_filenames @@ -108,6 +110,8 @@ 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" ) + # Python 3.7: rename doesn't return the new name: + misnamed = 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 From 3d6567571f483daf28d67bb0745a8fa55bddedf8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 20:35:59 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index e4bfb6a3..222a2830 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -13,10 +13,10 @@ from installer.records import RecordEntry from installer.utils import ( WheelFilename, + canonicalize_name, construct_record_file, copyfileobj_with_hashing, fix_shebang, - canonicalize_name, parse_entrypoints, parse_metadata_file, parse_wheel_filename,