55import stat
66import zipfile
77from contextlib import contextmanager
8- from typing import BinaryIO , ClassVar , Iterator , List , Tuple , Type , cast
8+ from typing import BinaryIO , ClassVar , Iterator , List , Optional , Tuple , Type , cast
99
10+ from installer .exceptions import InstallerError
1011from installer .records import RecordEntry , parse_record_file
1112from installer .utils import canonicalize_name , parse_wheel_filename
1213
@@ -101,17 +102,32 @@ def get_contents(self) -> Iterator[WheelContentElement]:
101102 raise NotImplementedError
102103
103104
104- class _WheelFileValidationError (ValueError ):
105+ class _WheelFileValidationError (ValueError , InstallerError ):
105106 """Raised when a wheel file fails validation."""
106107
107- def __init__ (self , issues : List [str ]) -> None : # noqa: D107
108+ def __init__ (self , issues : List [str ]) -> None :
108109 super ().__init__ (repr (issues ))
109110 self .issues = issues
110111
111112 def __repr__ (self ) -> str :
112113 return f"WheelFileValidationError(issues={ self .issues !r} )"
113114
114115
116+ class _WheelFileBadDistInfo (ValueError , InstallerError ):
117+ """Raised when a wheel file has issues around `.dist-info`."""
118+
119+ def __init__ (self , * , reason : str , filename : Optional [str ], dist_info : str ) -> None :
120+ super ().__init__ (reason )
121+ self .reason = reason
122+ self .filename = filename
123+ self .dist_info = dist_info
124+
125+ def __str__ (self ) -> str :
126+ return (
127+ f"{ self .reason } (filename={ self .filename !r} , dist_info={ self .dist_info !r} )"
128+ )
129+
130+
115131class WheelFile (WheelSource ):
116132 """Implements `WheelSource`, for an existing file from the filesystem.
117133
@@ -137,6 +153,7 @@ def __init__(self, f: zipfile.ZipFile) -> None:
137153 version = parsed_name .version ,
138154 distribution = parsed_name .distribution ,
139155 )
156+ self ._dist_info_dir : Optional [str ] = None
140157
141158 @classmethod
142159 @contextmanager
@@ -148,29 +165,39 @@ def open(cls, path: "os.PathLike[str]") -> Iterator["WheelFile"]:
148165 @property
149166 def dist_info_dir (self ) -> str :
150167 """Name of the dist-info directory."""
151- if not hasattr (self , "_dist_info_dir" ):
152- top_level_directories = {
153- path .split ("/" , 1 )[0 ] for path in self ._zipfile .namelist ()
154- }
155- dist_infos = [
156- name for name in top_level_directories if name .endswith (".dist-info" )
157- ]
158-
159- assert (
160- len (dist_infos ) == 1
161- ), "Wheel doesn't contain exactly one .dist-info directory"
162- dist_info_dir = dist_infos [0 ]
163-
164- # NAME-VER.dist-info
165- di_dname = dist_info_dir .rsplit ("-" , 2 )[0 ]
166- norm_di_dname = canonicalize_name (di_dname )
167- norm_file_dname = canonicalize_name (self .distribution )
168- assert (
169- norm_di_dname == norm_file_dname
170- ), "Wheel .dist-info directory doesn't match wheel filename"
171-
172- self ._dist_info_dir = dist_info_dir
173- return self ._dist_info_dir
168+ if self ._dist_info_dir is not None :
169+ return self ._dist_info_dir
170+
171+ top_level_directories = {
172+ path .split ("/" , 1 )[0 ] for path in self ._zipfile .namelist ()
173+ }
174+ dist_infos = [
175+ name for name in top_level_directories if name .endswith (".dist-info" )
176+ ]
177+
178+ try :
179+ (dist_info_dir ,) = dist_infos
180+ except ValueError :
181+ raise _WheelFileBadDistInfo (
182+ reason = "Wheel doesn't contain exactly one .dist-info directory" ,
183+ filename = self ._zipfile .filename ,
184+ dist_info = str (sorted (dist_infos )),
185+ ) from None
186+
187+ # NAME-VER.dist-info
188+ di_dname = dist_info_dir .rsplit ("-" , 2 )[0 ]
189+ norm_di_dname = canonicalize_name (di_dname )
190+ norm_file_dname = canonicalize_name (self .distribution )
191+
192+ if norm_di_dname != norm_file_dname :
193+ raise _WheelFileBadDistInfo (
194+ reason = "Wheel .dist-info directory doesn't match wheel filename" ,
195+ filename = self ._zipfile .filename ,
196+ dist_info = dist_info_dir ,
197+ )
198+
199+ self ._dist_info_dir = dist_info_dir
200+ return dist_info_dir
174201
175202 @property
176203 def dist_info_filenames (self ) -> List [str ]:
0 commit comments