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,30 @@ def __repr__(self):
341
343
return f'<FileHash mode: { self .mode } value: { self .value } >'
342
344
343
345
344
- class Distribution :
346
+ class DeprecatedNonAbstract :
347
+ def __new__ (cls , * args , ** kwargs ):
348
+ all_names = {
349
+ name for subclass in inspect .getmro (cls ) for name in vars (subclass )
350
+ }
351
+ abstract = {
352
+ name
353
+ for name in all_names
354
+ if getattr (getattr (cls , name ), '__isabstractmethod__' , False )
355
+ }
356
+ if abstract :
357
+ warnings .warn (
358
+ f"Unimplemented abstract methods { abstract } " ,
359
+ DeprecationWarning ,
360
+ stacklevel = 2 ,
361
+ )
362
+ return super ().__new__ (cls )
363
+
364
+
365
+ class Distribution (DeprecatedNonAbstract ):
345
366
"""A Python distribution package."""
346
367
347
368
@abc .abstractmethod
348
- def read_text (self , filename ):
369
+ def read_text (self , filename ) -> Optional [ str ] :
349
370
"""Attempt to load metadata file given by the name.
350
371
351
372
:param filename: The name of the file in the distribution info.
@@ -419,14 +440,15 @@ def metadata(self) -> _meta.PackageMetadata:
419
440
The returned object will have keys that name the various bits of
420
441
metadata. See PEP 566 for details.
421
442
"""
422
- text = (
443
+ opt_text = (
423
444
self .read_text ('METADATA' )
424
445
or self .read_text ('PKG-INFO' )
425
446
# This last clause is here to support old egg-info files. Its
426
447
# effect is to just end up using the PathDistribution's self._path
427
448
# (which points to the egg-info file) attribute unchanged.
428
449
or self .read_text ('' )
429
450
)
451
+ text = cast (str , opt_text )
430
452
return _adapters .Message (email .message_from_string (text ))
431
453
432
454
@property
@@ -455,8 +477,8 @@ def files(self):
455
477
:return: List of PackagePath for this distribution or None
456
478
457
479
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.
480
+ (i.e. RECORD for dist-info, or installed-files .txt or
481
+ SOURCES.txt for egg-info) is missing.
460
482
Result may be empty if the metadata exists but is empty.
461
483
"""
462
484
@@ -469,9 +491,19 @@ def make_file(name, hash=None, size_str=None):
469
491
470
492
@pass_none
471
493
def make_files (lines ):
472
- return list ( starmap (make_file , csv .reader (lines ) ))
494
+ return starmap (make_file , csv .reader (lines ))
473
495
474
- return make_files (self ._read_files_distinfo () or self ._read_files_egginfo ())
496
+ @pass_none
497
+ def skip_missing_files (package_paths ):
498
+ return list (filter (lambda path : path .locate ().exists (), package_paths ))
499
+
500
+ return skip_missing_files (
501
+ make_files (
502
+ self ._read_files_distinfo ()
503
+ or self ._read_files_egginfo_installed ()
504
+ or self ._read_files_egginfo_sources ()
505
+ )
506
+ )
475
507
476
508
def _read_files_distinfo (self ):
477
509
"""
@@ -480,10 +512,43 @@ def _read_files_distinfo(self):
480
512
text = self .read_text ('RECORD' )
481
513
return text and text .splitlines ()
482
514
483
- def _read_files_egginfo (self ):
515
+ def _read_files_egginfo_installed (self ):
516
+ """
517
+ Read installed-files.txt and return lines in a similar
518
+ CSV-parsable format as RECORD: each file must be placed
519
+ relative to the site-packages directory, and must also be
520
+ quoted (since file names can contain literal commas).
521
+
522
+ This file is written when the package is installed by pip,
523
+ but it might not be written for other installation methods.
524
+ Hence, even if we can assume that this file is accurate
525
+ when it exists, we cannot assume that it always exists.
484
526
"""
485
- SOURCES.txt might contain literal commas, so wrap each line
486
- in quotes.
527
+ text = self .read_text ('installed-files.txt' )
528
+ # We need to prepend the .egg-info/ subdir to the lines in this file.
529
+ # But this subdir is only available in the PathDistribution's self._path
530
+ # which is not easily accessible from this base class...
531
+ subdir = getattr (self , '_path' , None )
532
+ if not text or not subdir :
533
+ return
534
+ with contextlib .suppress (Exception ):
535
+ ret = [
536
+ str ((subdir / line ).resolve ().relative_to (self .locate_file ('' )))
537
+ for line in text .splitlines ()
538
+ ]
539
+ return map ('"{}"' .format , ret )
540
+
541
+ def _read_files_egginfo_sources (self ):
542
+ """
543
+ Read SOURCES.txt and return lines in a similar CSV-parsable
544
+ format as RECORD: each file name must be quoted (since it
545
+ might contain literal commas).
546
+
547
+ Note that SOURCES.txt is not a reliable source for what
548
+ files are installed by a package. This file is generated
549
+ for a source archive, and the files that are present
550
+ there (e.g. setup.py) may not correctly reflect the files
551
+ that are present after the package has been installed.
487
552
"""
488
553
text = self .read_text ('SOURCES.txt' )
489
554
return text and map ('"{}"' .format , text .splitlines ())
@@ -886,8 +951,13 @@ def _top_level_declared(dist):
886
951
887
952
888
953
def _top_level_inferred (dist ):
889
- return {
890
- f .parts [0 ] if len (f .parts ) > 1 else f . with_suffix ( '' ). name
954
+ opt_names = {
955
+ f .parts [0 ] if len (f .parts ) > 1 else inspect . getmodulename ( f )
891
956
for f in always_iterable (dist .files )
892
- if f .suffix == ".py"
893
957
}
958
+
959
+ @pass_none
960
+ def importable_name (name ):
961
+ return '.' not in name
962
+
963
+ return filter (importable_name , opt_names )
0 commit comments