77import pathlib
88import zipfile
99import operator
10+ import textwrap
1011import warnings
1112import functools
1213import itertools
1314import posixpath
1415import collections
1516
17+ from . import _adapters , _meta
18+ from ._collections import FreezableDefaultDict , Pair
19+ from ._functools import method_cache
1620from ._itertools import unique_everseen
1721
18- from configparser import ConfigParser
1922from contextlib import suppress
2023from importlib import import_module
2124from importlib .abc import MetaPathFinder
2225from itertools import starmap
23- from typing import Any , List , Mapping , Optional , Protocol , TypeVar , Union
26+ from typing import List , Mapping , Optional , Union
2427
2528
2629__all__ = [
@@ -51,6 +54,71 @@ def name(self):
5154 return name
5255
5356
57+ class Sectioned :
58+ """
59+ A simple entry point config parser for performance
60+
61+ >>> for item in Sectioned.read(Sectioned._sample):
62+ ... print(item)
63+ Pair(name='sec1', value='# comments ignored')
64+ Pair(name='sec1', value='a = 1')
65+ Pair(name='sec1', value='b = 2')
66+ Pair(name='sec2', value='a = 2')
67+
68+ >>> res = Sectioned.section_pairs(Sectioned._sample)
69+ >>> item = next(res)
70+ >>> item.name
71+ 'sec1'
72+ >>> item.value
73+ Pair(name='a', value='1')
74+ >>> item = next(res)
75+ >>> item.value
76+ Pair(name='b', value='2')
77+ >>> item = next(res)
78+ >>> item.name
79+ 'sec2'
80+ >>> item.value
81+ Pair(name='a', value='2')
82+ >>> list(res)
83+ []
84+ """
85+
86+ _sample = textwrap .dedent (
87+ """
88+ [sec1]
89+ # comments ignored
90+ a = 1
91+ b = 2
92+
93+ [sec2]
94+ a = 2
95+ """
96+ ).lstrip ()
97+
98+ @classmethod
99+ def section_pairs (cls , text ):
100+ return (
101+ section ._replace (value = Pair .parse (section .value ))
102+ for section in cls .read (text , filter_ = cls .valid )
103+ if section .name is not None
104+ )
105+
106+ @staticmethod
107+ def read (text , filter_ = None ):
108+ lines = filter (filter_ , map (str .strip , text .splitlines ()))
109+ name = None
110+ for value in lines :
111+ section_match = value .startswith ('[' ) and value .endswith (']' )
112+ if section_match :
113+ name = value .strip ('[]' )
114+ continue
115+ yield Pair (name , value )
116+
117+ @staticmethod
118+ def valid (line ):
119+ return line and not line .startswith ('#' )
120+
121+
54122class EntryPoint (
55123 collections .namedtuple ('EntryPointBase' , 'name value group' )):
56124 """An entry point as defined by Python packaging conventions.
@@ -108,22 +176,6 @@ def extras(self):
108176 match = self .pattern .match (self .value )
109177 return list (re .finditer (r'\w+' , match .group ('extras' ) or '' ))
110178
111- @classmethod
112- def _from_config (cls , config ):
113- return (
114- cls (name , value , group )
115- for group in config .sections ()
116- for name , value in config .items (group )
117- )
118-
119- @classmethod
120- def _from_text (cls , text ):
121- config = ConfigParser (delimiters = '=' )
122- # case sensitive: https://stackoverflow.com/q/1611799/812183
123- config .optionxform = str
124- config .read_string (text )
125- return cls ._from_config (config )
126-
127179 def _for (self , dist ):
128180 self .dist = dist
129181 return self
@@ -193,7 +245,18 @@ def groups(self):
193245
194246 @classmethod
195247 def _from_text_for (cls , text , dist ):
196- return cls (ep ._for (dist ) for ep in EntryPoint ._from_text (text ))
248+ return cls (ep ._for (dist ) for ep in cls ._from_text (text ))
249+
250+ @classmethod
251+ def _from_text (cls , text ):
252+ return itertools .starmap (EntryPoint , cls ._parse_groups (text or '' ))
253+
254+ @staticmethod
255+ def _parse_groups (text ):
256+ return (
257+ (item .value .name , item .value .value , item .name )
258+ for item in Sectioned .section_pairs (text )
259+ )
197260
198261
199262def flake8_bypass (func ):
@@ -259,7 +322,7 @@ def values(self):
259322 return super ().values ()
260323
261324
262- class SelectableGroups (dict ):
325+ class SelectableGroups (Deprecated , dict ):
263326 """
264327 A backward- and forward-compatible result from
265328 entry_points that fully implements the dict interface.
@@ -277,7 +340,8 @@ def _all(self):
277340 """
278341 Reconstruct a list of all entrypoints from the groups.
279342 """
280- return EntryPoints (itertools .chain .from_iterable (self .values ()))
343+ groups = super (Deprecated , self ).values ()
344+ return EntryPoints (itertools .chain .from_iterable (groups ))
281345
282346 @property
283347 def groups (self ):
@@ -322,25 +386,6 @@ def __repr__(self):
322386 return '<FileHash mode: {} value: {}>' .format (self .mode , self .value )
323387
324388
325- _T = TypeVar ("_T" )
326-
327-
328- class PackageMetadata (Protocol ):
329- def __len__ (self ) -> int :
330- ... # pragma: no cover
331-
332- def __contains__ (self , item : str ) -> bool :
333- ... # pragma: no cover
334-
335- def __getitem__ (self , key : str ) -> str :
336- ... # pragma: no cover
337-
338- def get_all (self , name : str , failobj : _T = ...) -> Union [List [Any ], _T ]:
339- """
340- Return all values associated with a possibly multi-valued key.
341- """
342-
343-
344389class Distribution :
345390 """A Python distribution package."""
346391
@@ -425,7 +470,7 @@ def _local(cls, root='.'):
425470 return PathDistribution (zipfile .Path (meta .build_as_zip (builder )))
426471
427472 @property
428- def metadata (self ) -> PackageMetadata :
473+ def metadata (self ) -> _meta . PackageMetadata :
429474 """Return the parsed metadata for this Distribution.
430475
431476 The returned object will have keys that name the various bits of
@@ -439,7 +484,7 @@ def metadata(self) -> PackageMetadata:
439484 # (which points to the egg-info file) attribute unchanged.
440485 or self .read_text ('' )
441486 )
442- return email .message_from_string (text )
487+ return _adapters . Message ( email .message_from_string (text ) )
443488
444489 @property
445490 def name (self ):
@@ -507,24 +552,7 @@ def _read_egg_info_reqs(self):
507552
508553 @classmethod
509554 def _deps_from_requires_text (cls , source ):
510- section_pairs = cls ._read_sections (source .splitlines ())
511- sections = {
512- section : list (map (operator .itemgetter ('line' ), results ))
513- for section , results in itertools .groupby (
514- section_pairs , operator .itemgetter ('section' )
515- )
516- }
517- return cls ._convert_egg_info_reqs_to_simple_reqs (sections )
518-
519- @staticmethod
520- def _read_sections (lines ):
521- section = None
522- for line in filter (None , lines ):
523- section_match = re .match (r'\[(.*)\]$' , line )
524- if section_match :
525- section = section_match .group (1 )
526- continue
527- yield locals ()
555+ return cls ._convert_egg_info_reqs_to_simple_reqs (Sectioned .read (source ))
528556
529557 @staticmethod
530558 def _convert_egg_info_reqs_to_simple_reqs (sections ):
@@ -549,9 +577,8 @@ def parse_condition(section):
549577 conditions = list (filter (None , [markers , make_condition (extra )]))
550578 return '; ' + ' and ' .join (conditions ) if conditions else ''
551579
552- for section , deps in sections .items ():
553- for dep in deps :
554- yield dep + parse_condition (section )
580+ for section in sections :
581+ yield section .value + parse_condition (section .name )
555582
556583
557584class DistributionFinder (MetaPathFinder ):
@@ -607,6 +634,10 @@ class FastPath:
607634 children.
608635 """
609636
637+ @functools .lru_cache () # type: ignore
638+ def __new__ (cls , root ):
639+ return super ().__new__ (cls )
640+
610641 def __init__ (self , root ):
611642 self .root = root
612643 self .base = os .path .basename (self .root ).lower ()
@@ -629,11 +660,53 @@ def zip_children(self):
629660 return dict .fromkeys (child .split (posixpath .sep , 1 )[0 ] for child in names )
630661
631662 def search (self , name ):
632- return (
633- self .joinpath (child )
634- for child in self .children ()
635- if name .matches (child , self .base )
663+ return self .lookup (self .mtime ).search (name )
664+
665+ @property
666+ def mtime (self ):
667+ with suppress (OSError ):
668+ return os .stat (self .root ).st_mtime
669+ self .lookup .cache_clear ()
670+
671+ @method_cache
672+ def lookup (self , mtime ):
673+ return Lookup (self )
674+
675+
676+ class Lookup :
677+ def __init__ (self , path : FastPath ):
678+ base = os .path .basename (path .root ).lower ()
679+ base_is_egg = base .endswith (".egg" )
680+ self .infos = FreezableDefaultDict (list )
681+ self .eggs = FreezableDefaultDict (list )
682+
683+ for child in path .children ():
684+ low = child .lower ()
685+ if low .endswith ((".dist-info" , ".egg-info" )):
686+ # rpartition is faster than splitext and suitable for this purpose.
687+ name = low .rpartition ("." )[0 ].partition ("-" )[0 ]
688+ normalized = Prepared .normalize (name )
689+ self .infos [normalized ].append (path .joinpath (child ))
690+ elif base_is_egg and low == "egg-info" :
691+ name = base .rpartition ("." )[0 ].partition ("-" )[0 ]
692+ legacy_normalized = Prepared .legacy_normalize (name )
693+ self .eggs [legacy_normalized ].append (path .joinpath (child ))
694+
695+ self .infos .freeze ()
696+ self .eggs .freeze ()
697+
698+ def search (self , prepared ):
699+ infos = (
700+ self .infos [prepared .normalized ]
701+ if prepared
702+ else itertools .chain .from_iterable (self .infos .values ())
636703 )
704+ eggs = (
705+ self .eggs [prepared .legacy_normalized ]
706+ if prepared
707+ else itertools .chain .from_iterable (self .eggs .values ())
708+ )
709+ return itertools .chain (infos , eggs )
637710
638711
639712class Prepared :
@@ -642,22 +715,14 @@ class Prepared:
642715 """
643716
644717 normalized = None
645- suffixes = 'dist-info' , 'egg-info'
646- exact_matches = ['' ][:0 ]
647- egg_prefix = ''
648- versionless_egg_name = ''
718+ legacy_normalized = None
649719
650720 def __init__ (self , name ):
651721 self .name = name
652722 if name is None :
653723 return
654724 self .normalized = self .normalize (name )
655- self .exact_matches = [
656- self .normalized + '.' + suffix for suffix in self .suffixes
657- ]
658- legacy_normalized = self .legacy_normalize (self .name )
659- self .egg_prefix = legacy_normalized + '-'
660- self .versionless_egg_name = legacy_normalized + '.egg'
725+ self .legacy_normalized = self .legacy_normalize (name )
661726
662727 @staticmethod
663728 def normalize (name ):
@@ -674,26 +739,8 @@ def legacy_normalize(name):
674739 """
675740 return name .lower ().replace ('-' , '_' )
676741
677- def matches (self , cand , base ):
678- low = cand .lower ()
679- # rpartition is faster than splitext and suitable for this purpose.
680- pre , _ , ext = low .rpartition ('.' )
681- name , _ , rest = pre .partition ('-' )
682- return (
683- low in self .exact_matches
684- or ext in self .suffixes
685- and (not self .normalized or name .replace ('.' , '_' ) == self .normalized )
686- # legacy case:
687- or self .is_egg (base )
688- and low == 'egg-info'
689- )
690-
691- def is_egg (self , base ):
692- return (
693- base == self .versionless_egg_name
694- or base .startswith (self .egg_prefix )
695- and base .endswith ('.egg' )
696- )
742+ def __bool__ (self ):
743+ return bool (self .name )
697744
698745
699746class MetadataPathFinder (DistributionFinder ):
@@ -718,6 +765,9 @@ def _search_paths(cls, name, paths):
718765 path .search (prepared ) for path in map (FastPath , paths )
719766 )
720767
768+ def invalidate_caches (cls ):
769+ FastPath .__new__ .cache_clear ()
770+
721771
722772class PathDistribution (Distribution ):
723773 def __init__ (self , path ):
@@ -761,7 +811,7 @@ def distributions(**kwargs):
761811 return Distribution .discover (** kwargs )
762812
763813
764- def metadata (distribution_name ) -> PackageMetadata :
814+ def metadata (distribution_name ) -> _meta . PackageMetadata :
765815 """Get the metadata for the named package.
766816
767817 :param distribution_name: The name of the distribution package to query.
0 commit comments