1
1
"""Lazy ZIP over HTTP"""
2
2
3
- __all__ = ['LazyZip ' ]
3
+ __all__ = ['dist_from_wheel_url ' ]
4
4
5
5
from bisect import bisect_left , bisect_right
6
6
from contextlib import contextmanager
12
12
13
13
from pip ._internal .network .utils import HEADERS , response_chunks
14
14
from pip ._internal .utils .typing import MYPY_CHECK_RUNNING
15
+ from pip ._internal .utils .wheel import pkg_resources_distribution_for_wheel
15
16
16
17
if MYPY_CHECK_RUNNING :
17
18
from typing import Any , Dict , Iterator , List , Optional , Tuple
18
19
20
+ from pip ._vendor .pkg_resources import Distribution
19
21
from pip ._vendor .requests .models import Response
20
22
21
23
from pip ._internal .network .session import PipSession
22
24
23
25
24
- class LazyZip :
26
+ def dist_from_wheel_url (name , url , session ):
27
+ # type: (str, str, PipSession) -> Distribution
28
+ """Return a pkg_resources.Distribution from the given wheel URL.
29
+
30
+ This uses HTTP range requests to only fetch the potion of the wheel
31
+ containing metadata, just enough for the object to be constructed.
32
+ If such requests are not supported, RuntimeError is raised.
33
+ """
34
+ with LazyZipOverHTTP (url , session ) as wheel :
35
+ # For read-only ZIP files, ZipFile only needs methods read,
36
+ # seek, seekable and tell, not the whole IO protocol.
37
+ zip_file = ZipFile (wheel ) # type: ignore
38
+ # After context manager exit, wheel.name
39
+ # is an invalid file by intention.
40
+ return pkg_resources_distribution_for_wheel (zip_file , name , wheel .name )
41
+
42
+
43
+ class LazyZipOverHTTP :
25
44
"""File-like object mapped to a ZIP file over HTTP.
26
45
27
46
This uses HTTP range requests to lazily fetch the file's content,
28
- which is supposed to be fed to ZipFile.
47
+ which is supposed to be fed to ZipFile. If such requests are not
48
+ supported by the server, raise RuntimeError during initialization.
29
49
"""
30
50
31
- def __init__ (self , session , url , chunk_size = CONTENT_CHUNK_SIZE ):
32
- # type: (PipSession, str , int) -> None
51
+ def __init__ (self , url , session , chunk_size = CONTENT_CHUNK_SIZE ):
52
+ # type: (str, PipSession , int) -> None
33
53
head = session .head (url , headers = HEADERS )
34
54
head .raise_for_status ()
35
55
assert head .status_code == 200
@@ -39,7 +59,9 @@ def __init__(self, session, url, chunk_size=CONTENT_CHUNK_SIZE):
39
59
self .truncate (self ._length )
40
60
self ._left = [] # type: List[int]
41
61
self ._right = [] # type: List[int]
42
- self ._check_zip ('bytes' in head .headers .get ('Accept-Ranges' , 'none' ))
62
+ if 'bytes' not in head .headers .get ('Accept-Ranges' , 'none' ):
63
+ raise RuntimeError ('range request is not supported' )
64
+ self ._check_zip ()
43
65
44
66
@property
45
67
def mode (self ):
@@ -50,7 +72,7 @@ def mode(self):
50
72
@property
51
73
def name (self ):
52
74
# type: () -> str
53
- """File name ."""
75
+ """Path to the underlying file ."""
54
76
return self ._file .name
55
77
56
78
def seekable (self ):
@@ -120,7 +142,7 @@ def writable(self):
120
142
return False
121
143
122
144
def __enter__ (self ):
123
- # type: () -> LazyZip
145
+ # type: () -> LazyZipOverHTTP
124
146
self ._file .__enter__ ()
125
147
return self
126
148
@@ -141,21 +163,16 @@ def _stay(self):
141
163
finally :
142
164
self .seek (pos )
143
165
144
- def _check_zip (self , range_request ):
145
- # type: (bool ) -> None
166
+ def _check_zip (self ):
167
+ # type: () -> None
146
168
"""Check and download until the file is a valid ZIP."""
147
169
end = self ._length - 1
148
- if not range_request :
149
- self ._download (0 , end )
150
- return
151
170
for start in reversed (range (0 , end , self ._chunk_size )):
152
171
self ._download (start , end )
153
172
with self ._stay ():
154
173
try :
155
174
# For read-only ZIP files, ZipFile only needs
156
175
# methods read, seek, seekable and tell.
157
- # The best way to type-hint in this case is to use
158
- # Python 3.8+ typing.Protocol.
159
176
ZipFile (self ) # type: ignore
160
177
except BadZipfile :
161
178
pass
0 commit comments