1
1
import email .message
2
+ import email .parser
2
3
import logging
3
4
import os
4
5
import 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
7
8
8
9
from pip ._vendor import pkg_resources
9
10
from pip ._vendor .packaging .requirements import Requirement
10
11
from pip ._vendor .packaging .utils import NormalizedName , canonicalize_name
11
12
from pip ._vendor .packaging .version import parse as parse_version
12
13
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
17
17
18
18
from .base import (
19
19
BaseDistribution ,
@@ -33,6 +33,41 @@ class EntryPoint(NamedTuple):
33
33
group : str
34
34
35
35
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
+
36
71
class Distribution (BaseDistribution ):
37
72
def __init__ (self , dist : pkg_resources .Distribution ) -> None :
38
73
self ._dist = dist
@@ -63,12 +98,26 @@ def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
63
98
64
99
:raises InvalidWheel: Whenever loading of the wheel causes a
65
100
:py:exc:`zipfile.BadZipFile` exception to be thrown.
101
+ :raises UnsupportedWheel: If the wheel is a valid zip, but malformed
102
+ internally.
66
103
"""
67
104
try :
68
105
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 :
71
113
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
+ )
72
121
return cls (dist )
73
122
74
123
@property
@@ -97,25 +146,6 @@ def canonical_name(self) -> NormalizedName:
97
146
def version (self ) -> DistributionVersion :
98
147
return parse_version (self ._dist .version )
99
148
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
-
119
149
def is_file (self , path : InfoPath ) -> bool :
120
150
return self ._dist .has_metadata (str (path ))
121
151
@@ -132,7 +162,10 @@ def read_text(self, path: InfoPath) -> str:
132
162
name = str (path )
133
163
if not self ._dist .has_metadata (name ):
134
164
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
136
169
137
170
def iter_entry_points (self ) -> Iterable [BaseEntryPoint ]:
138
171
for group , entries in self ._dist .get_entry_map ().items ():
@@ -142,7 +175,26 @@ def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
142
175
143
176
@property
144
177
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 ()
146
198
147
199
def iter_dependencies (self , extras : Collection [str ] = ()) -> Iterable [Requirement ]:
148
200
if extras : # pkg_resources raises on invalid extras, so we sanitize.
@@ -178,7 +230,6 @@ def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
178
230
return None
179
231
180
232
def get_distribution (self , name : str ) -> Optional [BaseDistribution ]:
181
-
182
233
# Search the distribution by looking through the working set.
183
234
dist = self ._search_distribution (name )
184
235
if dist :
0 commit comments