1
- import io
2
1
import os
3
2
import re
4
3
import abc
18
17
from importlib import import_module
19
18
from importlib .abc import MetaPathFinder
20
19
from itertools import starmap
20
+ from typing import Any , List , Optional , Protocol , TypeVar , Union
21
21
22
22
23
23
__all__ = [
31
31
'metadata' ,
32
32
'requires' ,
33
33
'version' ,
34
- ]
34
+ ]
35
35
36
36
37
37
class PackageNotFoundError (ModuleNotFoundError ):
@@ -43,7 +43,7 @@ def __str__(self):
43
43
44
44
@property
45
45
def name (self ):
46
- name , = self .args
46
+ ( name ,) = self .args
47
47
return name
48
48
49
49
@@ -60,7 +60,7 @@ class EntryPoint(
60
60
r'(?P<module>[\w.]+)\s*'
61
61
r'(:\s*(?P<attr>[\w.]+))?\s*'
62
62
r'(?P<extras>\[.*\])?\s*$'
63
- )
63
+ )
64
64
"""
65
65
A regular expression describing the syntax for an entry point,
66
66
which might look like:
@@ -77,6 +77,8 @@ class EntryPoint(
77
77
following the attr, and following any extras.
78
78
"""
79
79
80
+ dist : Optional ['Distribution' ] = None
81
+
80
82
def load (self ):
81
83
"""Load the entry point from its definition. If only a module
82
84
is indicated by the value, return that module. Otherwise,
@@ -104,23 +106,27 @@ def extras(self):
104
106
105
107
@classmethod
106
108
def _from_config (cls , config ):
107
- return [
109
+ return (
108
110
cls (name , value , group )
109
111
for group in config .sections ()
110
112
for name , value in config .items (group )
111
- ]
113
+ )
112
114
113
115
@classmethod
114
116
def _from_text (cls , text ):
115
117
config = ConfigParser (delimiters = '=' )
116
118
# case sensitive: https://stackoverflow.com/q/1611799/812183
117
119
config .optionxform = str
118
- try :
119
- config .read_string (text )
120
- except AttributeError : # pragma: nocover
121
- # Python 2 has no read_string
122
- config .readfp (io .StringIO (text ))
123
- return EntryPoint ._from_config (config )
120
+ config .read_string (text )
121
+ return cls ._from_config (config )
122
+
123
+ @classmethod
124
+ def _from_text_for (cls , text , dist ):
125
+ return (ep ._for (dist ) for ep in cls ._from_text (text ))
126
+
127
+ def _for (self , dist ):
128
+ self .dist = dist
129
+ return self
124
130
125
131
def __iter__ (self ):
126
132
"""
@@ -132,7 +138,7 @@ def __reduce__(self):
132
138
return (
133
139
self .__class__ ,
134
140
(self .name , self .value , self .group ),
135
- )
141
+ )
136
142
137
143
138
144
class PackagePath (pathlib .PurePosixPath ):
@@ -159,6 +165,25 @@ def __repr__(self):
159
165
return '<FileHash mode: {} value: {}>' .format (self .mode , self .value )
160
166
161
167
168
+ _T = TypeVar ("_T" )
169
+
170
+
171
+ class PackageMetadata (Protocol ):
172
+ def __len__ (self ) -> int :
173
+ ... # pragma: no cover
174
+
175
+ def __contains__ (self , item : str ) -> bool :
176
+ ... # pragma: no cover
177
+
178
+ def __getitem__ (self , key : str ) -> str :
179
+ ... # pragma: no cover
180
+
181
+ def get_all (self , name : str , failobj : _T = ...) -> Union [List [Any ], _T ]:
182
+ """
183
+ Return all values associated with a possibly multi-valued key.
184
+ """
185
+
186
+
162
187
class Distribution :
163
188
"""A Python distribution package."""
164
189
@@ -210,9 +235,8 @@ def discover(cls, **kwargs):
210
235
raise ValueError ("cannot accept context and kwargs" )
211
236
context = context or DistributionFinder .Context (** kwargs )
212
237
return itertools .chain .from_iterable (
213
- resolver (context )
214
- for resolver in cls ._discover_resolvers ()
215
- )
238
+ resolver (context ) for resolver in cls ._discover_resolvers ()
239
+ )
216
240
217
241
@staticmethod
218
242
def at (path ):
@@ -227,24 +251,24 @@ def at(path):
227
251
def _discover_resolvers ():
228
252
"""Search the meta_path for resolvers."""
229
253
declared = (
230
- getattr (finder , 'find_distributions' , None )
231
- for finder in sys .meta_path
232
- )
254
+ getattr (finder , 'find_distributions' , None ) for finder in sys .meta_path
255
+ )
233
256
return filter (None , declared )
234
257
235
258
@classmethod
236
259
def _local (cls , root = '.' ):
237
260
from pep517 import build , meta
261
+
238
262
system = build .compat_system (root )
239
263
builder = functools .partial (
240
264
meta .build ,
241
265
source_dir = root ,
242
266
system = system ,
243
- )
267
+ )
244
268
return PathDistribution (zipfile .Path (meta .build_as_zip (builder )))
245
269
246
270
@property
247
- def metadata (self ):
271
+ def metadata (self ) -> PackageMetadata :
248
272
"""Return the parsed metadata for this Distribution.
249
273
250
274
The returned object will have keys that name the various bits of
@@ -257,17 +281,22 @@ def metadata(self):
257
281
# effect is to just end up using the PathDistribution's self._path
258
282
# (which points to the egg-info file) attribute unchanged.
259
283
or self .read_text ('' )
260
- )
284
+ )
261
285
return email .message_from_string (text )
262
286
287
+ @property
288
+ def name (self ):
289
+ """Return the 'Name' metadata for the distribution package."""
290
+ return self .metadata ['Name' ]
291
+
263
292
@property
264
293
def version (self ):
265
294
"""Return the 'Version' metadata for the distribution package."""
266
295
return self .metadata ['Version' ]
267
296
268
297
@property
269
298
def entry_points (self ):
270
- return EntryPoint ._from_text (self .read_text ('entry_points.txt' ))
299
+ return list ( EntryPoint ._from_text_for (self .read_text ('entry_points.txt' ), self ))
271
300
272
301
@property
273
302
def files (self ):
@@ -324,9 +353,10 @@ def _deps_from_requires_text(cls, source):
324
353
section_pairs = cls ._read_sections (source .splitlines ())
325
354
sections = {
326
355
section : list (map (operator .itemgetter ('line' ), results ))
327
- for section , results in
328
- itertools .groupby (section_pairs , operator .itemgetter ('section' ))
329
- }
356
+ for section , results in itertools .groupby (
357
+ section_pairs , operator .itemgetter ('section' )
358
+ )
359
+ }
330
360
return cls ._convert_egg_info_reqs_to_simple_reqs (sections )
331
361
332
362
@staticmethod
@@ -350,6 +380,7 @@ def _convert_egg_info_reqs_to_simple_reqs(sections):
350
380
requirement. This method converts the former to the
351
381
latter. See _test_deps_from_requires_text for an example.
352
382
"""
383
+
353
384
def make_condition (name ):
354
385
return name and 'extra == "{name}"' .format (name = name )
355
386
@@ -438,48 +469,69 @@ def zip_children(self):
438
469
names = zip_path .root .namelist ()
439
470
self .joinpath = zip_path .joinpath
440
471
441
- return dict .fromkeys (
442
- child .split (posixpath .sep , 1 )[0 ]
443
- for child in names
444
- )
445
-
446
- def is_egg (self , search ):
447
- base = self .base
448
- return (
449
- base == search .versionless_egg_name
450
- or base .startswith (search .prefix )
451
- and base .endswith ('.egg' ))
472
+ return dict .fromkeys (child .split (posixpath .sep , 1 )[0 ] for child in names )
452
473
453
474
def search (self , name ):
454
- for child in self .children ():
455
- n_low = child .lower ()
456
- if (n_low in name .exact_matches
457
- or n_low .startswith (name .prefix )
458
- and n_low .endswith (name .suffixes )
459
- # legacy case:
460
- or self .is_egg (name ) and n_low == 'egg-info' ):
461
- yield self .joinpath (child )
475
+ return (
476
+ self .joinpath (child )
477
+ for child in self .children ()
478
+ if name .matches (child , self .base )
479
+ )
462
480
463
481
464
482
class Prepared :
465
483
"""
466
484
A prepared search for metadata on a possibly-named package.
467
485
"""
468
- normalized = ''
469
- prefix = ''
486
+
487
+ normalized = None
470
488
suffixes = '.dist-info' , '.egg-info'
471
489
exact_matches = ['' ][:0 ]
472
- versionless_egg_name = ''
473
490
474
491
def __init__ (self , name ):
475
492
self .name = name
476
493
if name is None :
477
494
return
478
- self .normalized = name .lower ().replace ('-' , '_' )
479
- self .prefix = self .normalized + '-'
480
- self .exact_matches = [
481
- self .normalized + suffix for suffix in self .suffixes ]
482
- self .versionless_egg_name = self .normalized + '.egg'
495
+ self .normalized = self .normalize (name )
496
+ self .exact_matches = [self .normalized + suffix for suffix in self .suffixes ]
497
+
498
+ @staticmethod
499
+ def normalize (name ):
500
+ """
501
+ PEP 503 normalization plus dashes as underscores.
502
+ """
503
+ return re .sub (r"[-_.]+" , "-" , name ).lower ().replace ('-' , '_' )
504
+
505
+ @staticmethod
506
+ def legacy_normalize (name ):
507
+ """
508
+ Normalize the package name as found in the convention in
509
+ older packaging tools versions and specs.
510
+ """
511
+ return name .lower ().replace ('-' , '_' )
512
+
513
+ def matches (self , cand , base ):
514
+ low = cand .lower ()
515
+ pre , ext = os .path .splitext (low )
516
+ name , sep , rest = pre .partition ('-' )
517
+ return (
518
+ low in self .exact_matches
519
+ or ext in self .suffixes
520
+ and (not self .normalized or name .replace ('.' , '_' ) == self .normalized )
521
+ # legacy case:
522
+ or self .is_egg (base )
523
+ and low == 'egg-info'
524
+ )
525
+
526
+ def is_egg (self , base ):
527
+ normalized = self .legacy_normalize (self .name or '' )
528
+ prefix = normalized + '-' if normalized else ''
529
+ versionless_egg_name = normalized + '.egg' if self .name else ''
530
+ return (
531
+ base == versionless_egg_name
532
+ or base .startswith (prefix )
533
+ and base .endswith ('.egg' )
534
+ )
483
535
484
536
485
537
class MetadataPathFinder (DistributionFinder ):
@@ -500,9 +552,8 @@ def find_distributions(cls, context=DistributionFinder.Context()):
500
552
def _search_paths (cls , name , paths ):
501
553
"""Find metadata directories in paths heuristically."""
502
554
return itertools .chain .from_iterable (
503
- path .search (Prepared (name ))
504
- for path in map (FastPath , paths )
505
- )
555
+ path .search (Prepared (name )) for path in map (FastPath , paths )
556
+ )
506
557
507
558
508
559
class PathDistribution (Distribution ):
@@ -515,9 +566,15 @@ def __init__(self, path):
515
566
self ._path = path
516
567
517
568
def read_text (self , filename ):
518
- with suppress (FileNotFoundError , IsADirectoryError , KeyError ,
519
- NotADirectoryError , PermissionError ):
569
+ with suppress (
570
+ FileNotFoundError ,
571
+ IsADirectoryError ,
572
+ KeyError ,
573
+ NotADirectoryError ,
574
+ PermissionError ,
575
+ ):
520
576
return self ._path .joinpath (filename ).read_text (encoding = 'utf-8' )
577
+
521
578
read_text .__doc__ = Distribution .read_text .__doc__
522
579
523
580
def locate_file (self , path ):
@@ -541,11 +598,11 @@ def distributions(**kwargs):
541
598
return Distribution .discover (** kwargs )
542
599
543
600
544
- def metadata (distribution_name ):
601
+ def metadata (distribution_name ) -> PackageMetadata :
545
602
"""Get the metadata for the named package.
546
603
547
604
:param distribution_name: The name of the distribution package to query.
548
- :return: An email.Message containing the parsed metadata.
605
+ :return: A PackageMetadata containing the parsed metadata.
549
606
"""
550
607
return Distribution .from_name (distribution_name ).metadata
551
608
@@ -565,15 +622,11 @@ def entry_points():
565
622
566
623
:return: EntryPoint objects for all installed packages.
567
624
"""
568
- eps = itertools .chain .from_iterable (
569
- dist .entry_points for dist in distributions ())
625
+ eps = itertools .chain .from_iterable (dist .entry_points for dist in distributions ())
570
626
by_group = operator .attrgetter ('group' )
571
627
ordered = sorted (eps , key = by_group )
572
628
grouped = itertools .groupby (ordered , by_group )
573
- return {
574
- group : tuple (eps )
575
- for group , eps in grouped
576
- }
629
+ return {group : tuple (eps ) for group , eps in grouped }
577
630
578
631
579
632
def files (distribution_name ):
0 commit comments