11import email .message
2+ import email .parser
23import logging
34import os
45import pathlib
5- from typing import Collection , Iterable , Iterator , List , NamedTuple , Optional
6- from zipfile import BadZipFile
6+ import zipfile
7+ from typing import Collection , Iterable , Iterator , List , Mapping , NamedTuple , Optional
78
89from pip ._vendor import pkg_resources
910from pip ._vendor .packaging .requirements import Requirement
1011from pip ._vendor .packaging .utils import NormalizedName , canonicalize_name
1112from pip ._vendor .packaging .version import parse as parse_version
1213
13- from pip ._internal .exceptions import InvalidWheel
14- from pip ._internal .utils import misc # TODO: Move definition here.
15- from pip ._internal .utils .packaging import get_installer , get_metadata
16- from pip ._internal .utils .wheel import pkg_resources_distribution_for_wheel
14+ from pip ._internal .exceptions import InvalidWheel , NoneMetadataError , UnsupportedWheel
15+ from pip ._internal .utils .misc import display_path
16+ from pip ._internal .utils .wheel import parse_wheel , read_wheel_metadata_file
1717
1818from .base import (
1919 BaseDistribution ,
@@ -33,6 +33,41 @@ class EntryPoint(NamedTuple):
3333 group : str
3434
3535
36+ class WheelMetadata :
37+ """IMetadataProvider that reads metadata files from a dictionary.
38+
39+ This also maps metadata decoding exceptions to our internal exception type.
40+ """
41+
42+ def __init__ (self , metadata : Mapping [str , bytes ], wheel_name : str ) -> None :
43+ self ._metadata = metadata
44+ self ._wheel_name = wheel_name
45+
46+ def has_metadata (self , name : str ) -> bool :
47+ return name in self ._metadata
48+
49+ def get_metadata (self , name : str ) -> str :
50+ try :
51+ return self ._metadata [name ].decode ()
52+ except UnicodeDecodeError as e :
53+ # Augment the default error with the origin of the file.
54+ raise UnsupportedWheel (
55+ f"Error decoding metadata for { self ._wheel_name } : { e } in { name } file"
56+ )
57+
58+ def get_metadata_lines (self , name : str ) -> Iterable [str ]:
59+ return pkg_resources .yield_lines (self .get_metadata (name ))
60+
61+ def metadata_isdir (self , name : str ) -> bool :
62+ return False
63+
64+ def metadata_listdir (self , name : str ) -> List [str ]:
65+ return []
66+
67+ def run_script (self , script_name : str , namespace : str ) -> None :
68+ pass
69+
70+
3671class Distribution (BaseDistribution ):
3772 def __init__ (self , dist : pkg_resources .Distribution ) -> None :
3873 self ._dist = dist
@@ -63,12 +98,26 @@ def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
6398
6499 :raises InvalidWheel: Whenever loading of the wheel causes a
65100 :py:exc:`zipfile.BadZipFile` exception to be thrown.
101+ :raises UnsupportedWheel: If the wheel is a valid zip, but malformed
102+ internally.
66103 """
67104 try :
68105 with wheel .as_zipfile () as zf :
69- dist = pkg_resources_distribution_for_wheel (zf , name , wheel .location )
70- except BadZipFile as e :
106+ info_dir , _ = parse_wheel (zf , name )
107+ metadata_text = {
108+ path .split ("/" , 1 )[- 1 ]: read_wheel_metadata_file (zf , path )
109+ for path in zf .namelist ()
110+ if path .startswith (f"{ info_dir } /" )
111+ }
112+ except zipfile .BadZipFile as e :
71113 raise InvalidWheel (wheel .location , name ) from e
114+ except UnsupportedWheel as e :
115+ raise UnsupportedWheel (f"{ name } has an invalid wheel, { e } " )
116+ dist = pkg_resources .DistInfoDistribution (
117+ location = wheel .location ,
118+ metadata = WheelMetadata (metadata_text , wheel .location ),
119+ project_name = name ,
120+ )
72121 return cls (dist )
73122
74123 @property
@@ -97,25 +146,6 @@ def canonical_name(self) -> NormalizedName:
97146 def version (self ) -> DistributionVersion :
98147 return parse_version (self ._dist .version )
99148
100- @property
101- def installer (self ) -> str :
102- try :
103- return get_installer (self ._dist )
104- except (OSError , ValueError ):
105- return "" # Fail silently if the installer file cannot be read.
106-
107- @property
108- def local (self ) -> bool :
109- return misc .dist_is_local (self ._dist )
110-
111- @property
112- def in_usersite (self ) -> bool :
113- return misc .dist_in_usersite (self ._dist )
114-
115- @property
116- def in_site_packages (self ) -> bool :
117- return misc .dist_in_site_packages (self ._dist )
118-
119149 def is_file (self , path : InfoPath ) -> bool :
120150 return self ._dist .has_metadata (str (path ))
121151
@@ -132,7 +162,10 @@ def read_text(self, path: InfoPath) -> str:
132162 name = str (path )
133163 if not self ._dist .has_metadata (name ):
134164 raise FileNotFoundError (name )
135- return self ._dist .get_metadata (name )
165+ content = self ._dist .get_metadata (name )
166+ if content is None :
167+ raise NoneMetadataError (self , name )
168+ return content
136169
137170 def iter_entry_points (self ) -> Iterable [BaseEntryPoint ]:
138171 for group , entries in self ._dist .get_entry_map ().items ():
@@ -142,7 +175,26 @@ def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
142175
143176 @property
144177 def metadata (self ) -> email .message .Message :
145- return get_metadata (self ._dist )
178+ """
179+ :raises NoneMetadataError: if the distribution reports `has_metadata()`
180+ True but `get_metadata()` returns None.
181+ """
182+ if isinstance (self ._dist , pkg_resources .DistInfoDistribution ):
183+ metadata_name = "METADATA"
184+ else :
185+ metadata_name = "PKG-INFO"
186+ try :
187+ metadata = self .read_text (metadata_name )
188+ except FileNotFoundError :
189+ if self .location :
190+ displaying_path = display_path (self .location )
191+ else :
192+ displaying_path = repr (self .location )
193+ logger .warning ("No metadata found in %s" , displaying_path )
194+ metadata = ""
195+ feed_parser = email .parser .FeedParser ()
196+ feed_parser .feed (metadata )
197+ return feed_parser .close ()
146198
147199 def iter_dependencies (self , extras : Collection [str ] = ()) -> Iterable [Requirement ]:
148200 if extras : # pkg_resources raises on invalid extras, so we sanitize.
@@ -178,7 +230,6 @@ def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
178230 return None
179231
180232 def get_distribution (self , name : str ) -> Optional [BaseDistribution ]:
181-
182233 # Search the distribution by looking through the working set.
183234 dist = self ._search_distribution (name )
184235 if dist :
0 commit comments