1
1
"""Support for installing and building the "wheel" binary package format.
2
2
"""
3
3
4
- # The following comment should be removed at some point in the future.
5
- # mypy: strict-optional=False
6
-
7
4
from __future__ import absolute_import
8
5
9
6
import collections
24
21
from pip ._vendor import pkg_resources
25
22
from pip ._vendor .distlib .scripts import ScriptMaker
26
23
from pip ._vendor .distlib .util import get_export_entry
27
- from pip ._vendor .six import StringIO
24
+ from pip ._vendor .six import (
25
+ PY2 ,
26
+ StringIO ,
27
+ ensure_str ,
28
+ ensure_text ,
29
+ itervalues ,
30
+ text_type ,
31
+ )
28
32
29
33
from pip ._internal .exceptions import InstallationError
30
34
from pip ._internal .locations import get_major_minor_version
43
47
from pip ._internal .utils .typing import cast
44
48
else :
45
49
from email .message import Message
46
- import typing # noqa F401
47
50
from typing import (
48
- Dict , List , Optional , Sequence , Tuple , Any ,
49
- Iterable , Iterator , Callable , Set , IO , cast
51
+ Any ,
52
+ Callable ,
53
+ Dict ,
54
+ IO ,
55
+ Iterable ,
56
+ Iterator ,
57
+ List ,
58
+ NewType ,
59
+ Optional ,
60
+ Sequence ,
61
+ Set ,
62
+ Tuple ,
63
+ Union ,
64
+ cast ,
50
65
)
51
66
52
67
from pip ._internal .models .scheme import Scheme
53
68
from pip ._internal .utils .filesystem import NamedTemporaryFileResult
54
69
55
- InstalledCSVRow = Tuple [str , ...]
70
+ RecordPath = NewType ('RecordPath' , text_type )
71
+ InstalledCSVRow = Tuple [RecordPath , str , Union [int , str ]]
56
72
57
73
58
74
logger = logging .getLogger (__name__ )
59
75
60
76
61
- def normpath (src , p ):
62
- # type: (str, str) -> str
63
- return os .path .relpath (src , p ).replace (os .path .sep , '/' )
64
-
65
-
66
77
def rehash (path , blocksize = 1 << 20 ):
67
- # type: (str , int) -> Tuple[str, str]
78
+ # type: (text_type , int) -> Tuple[str, str]
68
79
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
69
80
h , length = hash_file (path , blocksize )
70
81
digest = 'sha256=' + urlsafe_b64encode (
@@ -79,14 +90,14 @@ def csv_io_kwargs(mode):
79
90
"""Return keyword arguments to properly open a CSV file
80
91
in the given mode.
81
92
"""
82
- if sys . version_info . major < 3 :
93
+ if PY2 :
83
94
return {'mode' : '{}b' .format (mode )}
84
95
else :
85
- return {'mode' : mode , 'newline' : '' }
96
+ return {'mode' : mode , 'newline' : '' , 'encoding' : 'utf-8' }
86
97
87
98
88
99
def fix_script (path ):
89
- # type: (str ) -> Optional[bool]
100
+ # type: (text_type ) -> Optional[bool]
90
101
"""Replace #!python with #!/path/to/python
91
102
Return True if file was changed.
92
103
"""
@@ -217,9 +228,12 @@ def message_about_scripts_not_on_PATH(scripts):
217
228
return "\n " .join (msg_lines )
218
229
219
230
220
- def sorted_outrows (outrows ):
221
- # type: (Iterable[InstalledCSVRow]) -> List[InstalledCSVRow]
222
- """Return the given rows of a RECORD file in sorted order.
231
+ def _normalized_outrows (outrows ):
232
+ # type: (Iterable[InstalledCSVRow]) -> List[Tuple[str, str, str]]
233
+ """Normalize the given rows of a RECORD file.
234
+
235
+ Items in each row are converted into str. Rows are then sorted to make
236
+ the value more predictable for tests.
223
237
224
238
Each row is a 3-tuple (path, hash, size) and corresponds to a record of
225
239
a RECORD file (see PEP 376 and PEP 427 for details). For the rows
@@ -234,13 +248,35 @@ def sorted_outrows(outrows):
234
248
# coerce each element to a string to avoid a TypeError in this case.
235
249
# For additional background, see--
236
250
# https://github.com/pypa/pip/issues/5868
237
- return sorted (outrows , key = lambda row : tuple (str (x ) for x in row ))
251
+ return sorted (
252
+ (ensure_str (record_path , encoding = 'utf-8' ), hash_ , str (size ))
253
+ for record_path , hash_ , size in outrows
254
+ )
255
+
256
+
257
+ def _record_to_fs_path (record_path ):
258
+ # type: (RecordPath) -> text_type
259
+ return record_path
260
+
261
+
262
+ def _fs_to_record_path (path , relative_to = None ):
263
+ # type: (text_type, Optional[text_type]) -> RecordPath
264
+ if relative_to is not None :
265
+ path = os .path .relpath (path , relative_to )
266
+ path = path .replace (os .path .sep , '/' )
267
+ return cast ('RecordPath' , path )
268
+
269
+
270
+ def _parse_record_path (record_column ):
271
+ # type: (str) -> RecordPath
272
+ p = ensure_text (record_column , encoding = 'utf-8' )
273
+ return cast ('RecordPath' , p )
238
274
239
275
240
276
def get_csv_rows_for_installed (
241
277
old_csv_rows , # type: Iterable[List[str]]
242
- installed , # type: Dict[str, str ]
243
- changed , # type: Set[str ]
278
+ installed , # type: Dict[RecordPath, RecordPath ]
279
+ changed , # type: Set[RecordPath ]
244
280
generated , # type: List[str]
245
281
lib_dir , # type: str
246
282
):
@@ -255,21 +291,20 @@ def get_csv_rows_for_installed(
255
291
logger .warning (
256
292
'RECORD line has more than three elements: {}' .format (row )
257
293
)
258
- # Make a copy because we are mutating the row.
259
- row = list (row )
260
- old_path = row [0 ]
261
- new_path = installed .pop (old_path , old_path )
262
- row [0 ] = new_path
263
- if new_path in changed :
264
- digest , length = rehash (new_path )
265
- row [1 ] = digest
266
- row [2 ] = length
267
- installed_rows .append (tuple (row ))
294
+ old_record_path = _parse_record_path (row [0 ])
295
+ new_record_path = installed .pop (old_record_path , old_record_path )
296
+ if new_record_path in changed :
297
+ digest , length = rehash (_record_to_fs_path (new_record_path ))
298
+ else :
299
+ digest = row [1 ] if len (row ) > 1 else ''
300
+ length = row [2 ] if len (row ) > 2 else ''
301
+ installed_rows .append ((new_record_path , digest , length ))
268
302
for f in generated :
303
+ path = _fs_to_record_path (f , lib_dir )
269
304
digest , length = rehash (f )
270
- installed_rows .append ((normpath ( f , lib_dir ), digest , str ( length ) ))
271
- for f in installed :
272
- installed_rows .append ((installed [ f ] , '' , '' ))
305
+ installed_rows .append ((path , digest , length ))
306
+ for installed_record_path in itervalues ( installed ) :
307
+ installed_rows .append ((installed_record_path , '' , '' ))
273
308
return installed_rows
274
309
275
310
@@ -338,8 +373,8 @@ def install_unpacked_wheel(
338
373
# installed = files copied from the wheel to the destination
339
374
# changed = files changed while installing (scripts #! line typically)
340
375
# generated = files newly generated during the install (script wrappers)
341
- installed = {} # type: Dict[str, str ]
342
- changed = set ()
376
+ installed = {} # type: Dict[RecordPath, RecordPath ]
377
+ changed = set () # type: Set[RecordPath]
343
378
generated = [] # type: List[str]
344
379
345
380
# Compile all of the pyc files that we're going to be installing
@@ -351,20 +386,20 @@ def install_unpacked_wheel(
351
386
logger .debug (stdout .getvalue ())
352
387
353
388
def record_installed (srcfile , destfile , modified = False ):
354
- # type: (str, str , bool) -> None
389
+ # type: (text_type, text_type , bool) -> None
355
390
"""Map archive RECORD paths to installation RECORD paths."""
356
- oldpath = normpath (srcfile , wheeldir )
357
- newpath = normpath (destfile , lib_dir )
391
+ oldpath = _fs_to_record_path (srcfile , wheeldir )
392
+ newpath = _fs_to_record_path (destfile , lib_dir )
358
393
installed [oldpath ] = newpath
359
394
if modified :
360
- changed .add (destfile )
395
+ changed .add (_fs_to_record_path ( destfile ) )
361
396
362
397
def clobber (
363
- source , # type: str
364
- dest , # type: str
398
+ source , # type: text_type
399
+ dest , # type: text_type
365
400
is_base , # type: bool
366
- fixer = None , # type: Optional[Callable[[str ], Any]]
367
- filter = None # type: Optional[Callable[[str ], bool]]
401
+ fixer = None , # type: Optional[Callable[[text_type ], Any]]
402
+ filter = None # type: Optional[Callable[[text_type ], bool]]
368
403
):
369
404
# type: (...) -> None
370
405
ensure_dir (dest ) # common for the 'include' path
@@ -423,7 +458,11 @@ def clobber(
423
458
changed = fixer (destfile )
424
459
record_installed (srcfile , destfile , changed )
425
460
426
- clobber (source , lib_dir , True )
461
+ clobber (
462
+ ensure_text (source , encoding = sys .getfilesystemencoding ()),
463
+ ensure_text (lib_dir , encoding = sys .getfilesystemencoding ()),
464
+ True ,
465
+ )
427
466
428
467
dest_info_dir = os .path .join (lib_dir , info_dir )
429
468
@@ -432,7 +471,7 @@ def clobber(
432
471
console , gui = get_entrypoints (ep_file )
433
472
434
473
def is_entrypoint_wrapper (name ):
435
- # type: (str ) -> bool
474
+ # type: (text_type ) -> bool
436
475
# EP, EP.exe and EP-script.py are scripts generated for
437
476
# entry point EP by setuptools
438
477
if name .lower ().endswith ('.exe' ):
@@ -456,7 +495,13 @@ def is_entrypoint_wrapper(name):
456
495
filter = is_entrypoint_wrapper
457
496
source = os .path .join (wheeldir , datadir , subdir )
458
497
dest = getattr (scheme , subdir )
459
- clobber (source , dest , False , fixer = fixer , filter = filter )
498
+ clobber (
499
+ ensure_text (source , encoding = sys .getfilesystemencoding ()),
500
+ ensure_text (dest , encoding = sys .getfilesystemencoding ()),
501
+ False ,
502
+ fixer = fixer ,
503
+ filter = filter ,
504
+ )
460
505
461
506
maker = PipScriptMaker (None , scheme .scripts )
462
507
@@ -606,16 +651,11 @@ def _generate_file(path, **kwargs):
606
651
generated = generated ,
607
652
lib_dir = lib_dir )
608
653
with _generate_file (record_path , ** csv_io_kwargs ('w' )) as record_file :
609
-
610
- # The type mypy infers for record_file using reveal_type
611
- # is different for Python 3 (typing.IO[Any]) and
612
- # Python 2 (typing.BinaryIO), leading us to explicitly
613
- # cast to typing.IO[str] as a workaround
614
- # for bad Python 2 behaviour
615
- record_file_obj = cast ('IO[str]' , record_file )
616
-
617
- writer = csv .writer (record_file_obj )
618
- writer .writerows (sorted_outrows (rows )) # sort to simplify testing
654
+ # The type mypy infers for record_file is different for Python 3
655
+ # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly
656
+ # cast to typing.IO[str] as a workaround.
657
+ writer = csv .writer (cast ('IO[str]' , record_file ))
658
+ writer .writerows (_normalized_outrows (rows ))
619
659
620
660
621
661
def install_wheel (
0 commit comments