12
12
import functools
13
13
import itertools
14
14
import posixpath
15
+ import contextlib
15
16
import collections
17
+ import inspect
16
18
17
19
from . import _adapters , _meta
18
20
from ._collections import FreezableDefaultDict , Pair
24
26
from importlib import import_module
25
27
from importlib .abc import MetaPathFinder
26
28
from itertools import starmap
27
- from typing import List , Mapping , Optional
29
+ from typing import List , Mapping , Optional , cast
28
30
29
31
30
32
__all__ = [
@@ -341,11 +343,11 @@ def __repr__(self):
341
343
return f'<FileHash mode: { self .mode } value: { self .value } >'
342
344
343
345
344
- class Distribution :
346
+ class Distribution ( metaclass = abc . ABCMeta ) :
345
347
"""A Python distribution package."""
346
348
347
349
@abc .abstractmethod
348
- def read_text (self , filename ):
350
+ def read_text (self , filename ) -> Optional [ str ] :
349
351
"""Attempt to load metadata file given by the name.
350
352
351
353
:param filename: The name of the file in the distribution info.
@@ -419,14 +421,15 @@ def metadata(self) -> _meta.PackageMetadata:
419
421
The returned object will have keys that name the various bits of
420
422
metadata. See PEP 566 for details.
421
423
"""
422
- text = (
424
+ opt_text = (
423
425
self .read_text ('METADATA' )
424
426
or self .read_text ('PKG-INFO' )
425
427
# This last clause is here to support old egg-info files. Its
426
428
# effect is to just end up using the PathDistribution's self._path
427
429
# (which points to the egg-info file) attribute unchanged.
428
430
or self .read_text ('' )
429
431
)
432
+ text = cast (str , opt_text )
430
433
return _adapters .Message (email .message_from_string (text ))
431
434
432
435
@property
@@ -455,8 +458,8 @@ def files(self):
455
458
:return: List of PackagePath for this distribution or None
456
459
457
460
Result is `None` if the metadata file that enumerates files
458
- (i.e. RECORD for dist-info or SOURCES .txt for egg-info) is
459
- missing.
461
+ (i.e. RECORD for dist-info, or installed-files .txt or
462
+ SOURCES.txt for egg-info) is missing.
460
463
Result may be empty if the metadata exists but is empty.
461
464
"""
462
465
@@ -469,9 +472,19 @@ def make_file(name, hash=None, size_str=None):
469
472
470
473
@pass_none
471
474
def make_files (lines ):
472
- return list ( starmap (make_file , csv .reader (lines ) ))
475
+ return starmap (make_file , csv .reader (lines ))
473
476
474
- return make_files (self ._read_files_distinfo () or self ._read_files_egginfo ())
477
+ @pass_none
478
+ def skip_missing_files (package_paths ):
479
+ return list (filter (lambda path : path .locate ().exists (), package_paths ))
480
+
481
+ return skip_missing_files (
482
+ make_files (
483
+ self ._read_files_distinfo ()
484
+ or self ._read_files_egginfo_installed ()
485
+ or self ._read_files_egginfo_sources ()
486
+ )
487
+ )
475
488
476
489
def _read_files_distinfo (self ):
477
490
"""
@@ -480,10 +493,43 @@ def _read_files_distinfo(self):
480
493
text = self .read_text ('RECORD' )
481
494
return text and text .splitlines ()
482
495
483
- def _read_files_egginfo (self ):
496
+ def _read_files_egginfo_installed (self ):
484
497
"""
485
- SOURCES.txt might contain literal commas, so wrap each line
486
- in quotes.
498
+ Read installed-files.txt and return lines in a similar
499
+ CSV-parsable format as RECORD: each file must be placed
500
+ relative to the site-packages directory, and must also be
501
+ quoted (since file names can contain literal commas).
502
+
503
+ This file is written when the package is installed by pip,
504
+ but it might not be written for other installation methods.
505
+ Hence, even if we can assume that this file is accurate
506
+ when it exists, we cannot assume that it always exists.
507
+ """
508
+ text = self .read_text ('installed-files.txt' )
509
+ # We need to prepend the .egg-info/ subdir to the lines in this file.
510
+ # But this subdir is only available in the PathDistribution's self._path
511
+ # which is not easily accessible from this base class...
512
+ subdir = getattr (self , '_path' , None )
513
+ if not text or not subdir :
514
+ return
515
+ with contextlib .suppress (Exception ):
516
+ ret = [
517
+ str ((subdir / line ).resolve ().relative_to (self .locate_file ('' )))
518
+ for line in text .splitlines ()
519
+ ]
520
+ return map ('"{}"' .format , ret )
521
+
522
+ def _read_files_egginfo_sources (self ):
523
+ """
524
+ Read SOURCES.txt and return lines in a similar CSV-parsable
525
+ format as RECORD: each file name must be quoted (since it
526
+ might contain literal commas).
527
+
528
+ Note that SOURCES.txt is not a reliable source for what
529
+ files are installed by a package. This file is generated
530
+ for a source archive, and the files that are present
531
+ there (e.g. setup.py) may not correctly reflect the files
532
+ that are present after the package has been installed.
487
533
"""
488
534
text = self .read_text ('SOURCES.txt' )
489
535
return text and map ('"{}"' .format , text .splitlines ())
@@ -886,8 +932,13 @@ def _top_level_declared(dist):
886
932
887
933
888
934
def _top_level_inferred (dist ):
889
- return {
890
- f .parts [0 ] if len (f .parts ) > 1 else f . with_suffix ( '' ). name
935
+ opt_names = {
936
+ f .parts [0 ] if len (f .parts ) > 1 else inspect . getmodulename ( f )
891
937
for f in always_iterable (dist .files )
892
- if f .suffix == ".py"
893
938
}
939
+
940
+ @pass_none
941
+ def importable_name (name ):
942
+ return '.' not in name
943
+
944
+ return filter (importable_name , opt_names )
0 commit comments