1010import operator
1111import functools
1212import itertools
13+ import posixpath
1314import collections
1415
1516from ._compat import (
2324 NotADirectoryError ,
2425 PermissionError ,
2526 pathlib ,
26- PYPY_OPEN_BUG ,
2727 ModuleNotFoundError ,
2828 MetaPathFinder ,
2929 email_message_from_string ,
3030 PyPy_repr ,
31+ unique_ordered ,
32+ str ,
3133 )
3234from importlib import import_module
3335from itertools import starmap
@@ -95,6 +97,16 @@ def load(self):
9597 attrs = filter (None , (match .group ('attr' ) or '' ).split ('.' ))
9698 return functools .reduce (getattr , attrs , module )
9799
100+ @property
101+ def module (self ):
102+ match = self .pattern .match (self .value )
103+ return match .group ('module' )
104+
105+ @property
106+ def attr (self ):
107+ match = self .pattern .match (self .value )
108+ return match .group ('attr' )
109+
98110 @property
99111 def extras (self ):
100112 match = self .pattern .match (self .value )
@@ -400,10 +412,6 @@ def path(self):
400412 """
401413 return vars (self ).get ('path' , sys .path )
402414
403- @property
404- def pattern (self ):
405- return '.*' if self .name is None else re .escape (self .name )
406-
407415 @abc .abstractmethod
408416 def find_distributions (self , context = Context ()):
409417 """
@@ -415,6 +423,75 @@ def find_distributions(self, context=Context()):
415423 """
416424
417425
426+ class FastPath :
427+ """
428+ Micro-optimized class for searching a path for
429+ children.
430+ """
431+
432+ def __init__ (self , root ):
433+ self .root = str (root )
434+ self .base = os .path .basename (self .root ).lower ()
435+
436+ def joinpath (self , child ):
437+ return pathlib .Path (self .root , child )
438+
439+ def children (self ):
440+ with suppress (Exception ):
441+ return os .listdir (self .root or '' )
442+ with suppress (Exception ):
443+ return self .zip_children ()
444+ return []
445+
446+ def zip_children (self ):
447+ zip_path = zipp .Path (self .root )
448+ names = zip_path .root .namelist ()
449+ self .joinpath = zip_path .joinpath
450+
451+ return unique_ordered (
452+ child .split (posixpath .sep , 1 )[0 ]
453+ for child in names
454+ )
455+
456+ def is_egg (self , search ):
457+ base = self .base
458+ return (
459+ base == search .versionless_egg_name
460+ or base .startswith (search .prefix )
461+ and base .endswith ('.egg' ))
462+
463+ def search (self , name ):
464+ for child in self .children ():
465+ n_low = child .lower ()
466+ if (n_low in name .exact_matches
467+ or n_low .startswith (name .prefix )
468+ and n_low .endswith (name .suffixes )
469+ # legacy case:
470+ or self .is_egg (name ) and n_low == 'egg-info' ):
471+ yield self .joinpath (child )
472+
473+
474+ class Prepared :
475+ """
476+ A prepared search for metadata on a possibly-named package.
477+ """
478+ normalized = ''
479+ prefix = ''
480+ suffixes = '.dist-info' , '.egg-info'
481+ exact_matches = ['' ][:0 ]
482+ versionless_egg_name = ''
483+
484+ def __init__ (self , name ):
485+ self .name = name
486+ if name is None :
487+ return
488+ self .normalized = name .lower ().replace ('-' , '_' )
489+ self .prefix = self .normalized + '-'
490+ self .exact_matches = [
491+ self .normalized + suffix for suffix in self .suffixes ]
492+ self .versionless_egg_name = self .normalized + '.egg'
493+
494+
418495@install
419496class MetadataPathFinder (NullFinder , DistributionFinder ):
420497 """A degenerate finder for distribution packages on the file system.
@@ -432,45 +509,17 @@ def find_distributions(self, context=DistributionFinder.Context()):
432509 (or all names if ``None`` indicated) along the paths in the list
433510 of directories ``context.path``.
434511 """
435- found = self ._search_paths (context .pattern , context .path )
512+ found = self ._search_paths (context .name , context .path )
436513 return map (PathDistribution , found )
437514
438515 @classmethod
439- def _search_paths (cls , pattern , paths ):
516+ def _search_paths (cls , name , paths ):
440517 """Find metadata directories in paths heuristically."""
441518 return itertools .chain .from_iterable (
442- cls . _search_path ( path , pattern )
443- for path in map (cls . _switch_path , paths )
519+ path . search ( Prepared ( name ) )
520+ for path in map (FastPath , paths )
444521 )
445522
446- @staticmethod
447- def _switch_path (path ):
448- if not PYPY_OPEN_BUG or os .path .isfile (path ): # pragma: no branch
449- with suppress (Exception ):
450- return zipp .Path (path )
451- return pathlib .Path (path )
452-
453- @classmethod
454- def _matches_info (cls , normalized , item ):
455- template = r'{pattern}(-.*)?\.(dist|egg)-info'
456- manifest = template .format (pattern = normalized )
457- return re .match (manifest , item .name , flags = re .IGNORECASE )
458-
459- @classmethod
460- def _matches_legacy (cls , normalized , item ):
461- template = r'{pattern}-.*\.egg[\\/]EGG-INFO'
462- manifest = template .format (pattern = normalized )
463- return re .search (manifest , str (item ), flags = re .IGNORECASE )
464-
465- @classmethod
466- def _search_path (cls , root , pattern ):
467- if not root .is_dir ():
468- return ()
469- normalized = pattern .replace ('-' , '_' )
470- return (item for item in root .iterdir ()
471- if cls ._matches_info (normalized , item )
472- or cls ._matches_legacy (normalized , item ))
473-
474523
475524class PathDistribution (Distribution ):
476525 def __init__ (self , path ):
0 commit comments