11from __future__ import unicode_literals , absolute_import
22
33import io
4+ import os
45import re
56import abc
67import csv
1819 suppress ,
1920 map ,
2021 FileNotFoundError ,
22+ IsADirectoryError ,
2123 NotADirectoryError ,
24+ PermissionError ,
2225 pathlib ,
26+ PYPY_OPEN_BUG ,
2327 ModuleNotFoundError ,
2428 MetaPathFinder ,
2529 email_message_from_string ,
30+ ensure_is_path ,
2631 )
2732from importlib import import_module
2833from itertools import starmap
3338
3439__all__ = [
3540 'Distribution' ,
41+ 'DistributionFinder' ,
3642 'PackageNotFoundError' ,
3743 'distribution' ,
3844 'distributions' ,
@@ -102,7 +108,9 @@ def _from_config(cls, config):
102108
103109 @classmethod
104110 def _from_text (cls , text ):
105- config = ConfigParser ()
111+ config = ConfigParser (delimiters = '=' )
112+ # case sensitive: https://stackoverflow.com/q/1611799/812183
113+ config .optionxform = str
106114 try :
107115 config .read_string (text )
108116 except AttributeError : # pragma: nocover
@@ -170,24 +178,41 @@ def from_name(cls, name):
170178 metadata cannot be found.
171179 """
172180 for resolver in cls ._discover_resolvers ():
173- dists = resolver (name )
181+ dists = resolver (DistributionFinder . Context ( name = name ) )
174182 dist = next (dists , None )
175183 if dist is not None :
176184 return dist
177185 else :
178186 raise PackageNotFoundError (name )
179187
180188 @classmethod
181- def discover (cls ):
189+ def discover (cls , ** kwargs ):
182190 """Return an iterable of Distribution objects for all packages.
183191
192+ Pass a ``context`` or pass keyword arguments for constructing
193+ a context.
194+
195+ :context: A ``DistributionFinder.Context`` object.
184196 :return: Iterable of Distribution objects for all packages.
185197 """
198+ context = kwargs .pop ('context' , None )
199+ if context and kwargs :
200+ raise ValueError ("cannot accept context and kwargs" )
201+ context = context or DistributionFinder .Context (** kwargs )
186202 return itertools .chain .from_iterable (
187- resolver ()
203+ resolver (context )
188204 for resolver in cls ._discover_resolvers ()
189205 )
190206
207+ @staticmethod
208+ def at (path ):
209+ """Return a Distribution for the indicated metadata path
210+
211+ :param path: a string or path-like object
212+ :return: a concrete Distribution instance for the path
213+ """
214+ return PathDistribution (ensure_is_path (path ))
215+
191216 @staticmethod
192217 def _discover_resolvers ():
193218 """Search the meta_path for resolvers."""
@@ -199,15 +224,14 @@ def _discover_resolvers():
199224
200225 @classmethod
201226 def find_local (cls , root = '.' ):
202- import pep517 .build as build
203- import pep517 .build_meta as bm
227+ from pep517 import build , meta
204228 system = build .compat_system (root )
205229 builder = functools .partial (
206- bm . build_meta ,
230+ meta . build ,
207231 source_dir = root ,
208232 system = system ,
209233 )
210- return PathDistribution (zipp .Path (bm . build_meta_as_zip (builder )))
234+ return PathDistribution (zipp .Path (meta . build_as_zip (builder )))
211235
212236 @property
213237 def metadata (self ):
@@ -237,6 +261,15 @@ def entry_points(self):
237261
238262 @property
239263 def files (self ):
264+ """Files in this distribution.
265+
266+ :return: List of PackagePath for this distribution or None
267+
268+ Result is `None` if the metadata file that enumerates files
269+ (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
270+ missing.
271+ Result may be empty if the metadata exists but is empty.
272+ """
240273 file_lines = self ._read_files_distinfo () or self ._read_files_egginfo ()
241274
242275 def make_file (name , hash = None , size_str = None ):
@@ -246,7 +279,7 @@ def make_file(name, hash=None, size_str=None):
246279 result .dist = self
247280 return result
248281
249- return file_lines and starmap (make_file , csv .reader (file_lines ))
282+ return file_lines and list ( starmap (make_file , csv .reader (file_lines ) ))
250283
251284 def _read_files_distinfo (self ):
252285 """
@@ -266,11 +299,11 @@ def _read_files_egginfo(self):
266299 @property
267300 def requires (self ):
268301 """Generated requirements specified for this Distribution"""
269- return self ._read_dist_info_reqs () or self ._read_egg_info_reqs ()
302+ reqs = self ._read_dist_info_reqs () or self ._read_egg_info_reqs ()
303+ return reqs and list (reqs )
270304
271305 def _read_dist_info_reqs (self ):
272- spec = self .metadata ['Requires-Dist' ]
273- return spec and filter (None , spec .splitlines ())
306+ return self .metadata .get_all ('Requires-Dist' )
274307
275308 def _read_egg_info_reqs (self ):
276309 source = self .read_text ('requires.txt' )
@@ -328,15 +361,35 @@ class DistributionFinder(MetaPathFinder):
328361 A MetaPathFinder capable of discovering installed distributions.
329362 """
330363
364+ class Context :
365+
366+ name = None
367+ """
368+ Specific name for which a distribution finder should match.
369+ """
370+
371+ def __init__ (self , ** kwargs ):
372+ vars (self ).update (kwargs )
373+
374+ @property
375+ def path (self ):
376+ """
377+ The path that a distribution finder should search.
378+ """
379+ return vars (self ).get ('path' , sys .path )
380+
381+ @property
382+ def pattern (self ):
383+ return '.*' if self .name is None else re .escape (self .name )
384+
331385 @abc .abstractmethod
332- def find_distributions (self , name = None , path = None ):
386+ def find_distributions (self , context = Context () ):
333387 """
334388 Find distributions.
335389
336390 Return an iterable of all Distribution instances capable of
337- loading the metadata for packages matching the ``name``
338- (or all names if not supplied) along the paths in the list
339- of directories ``path`` (defaults to sys.path).
391+ loading the metadata for packages matching the ``context``,
392+ a DistributionFinder.Context instance.
340393 """
341394
342395
@@ -347,21 +400,17 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
347400 This finder supplies only a find_distributions() method for versions
348401 of Python that do not have a PathFinder find_distributions().
349402 """
350- search_template = r'(?:{pattern}(-.*)?\.(dist|egg)-info|EGG-INFO)'
351403
352- def find_distributions (self , name = None , path = None ):
404+ def find_distributions (self , context = DistributionFinder . Context () ):
353405 """
354406 Find distributions.
355407
356408 Return an iterable of all Distribution instances capable of
357- loading the metadata for packages matching the `` name``
358- (or all names if not supplied ) along the paths in the list
359- of directories ``path`` (defaults to sys.path) .
409+ loading the metadata for packages matching ``context. name``
410+ (or all names if ``None`` indicated ) along the paths in the list
411+ of directories ``context. path``.
360412 """
361- if path is None :
362- path = sys .path
363- pattern = '.*' if name is None else re .escape (name )
364- found = self ._search_paths (pattern , path )
413+ found = self ._search_paths (context .pattern , context .path )
365414 return map (PathDistribution , found )
366415
367416 @classmethod
@@ -374,80 +423,86 @@ def _search_paths(cls, pattern, paths):
374423
375424 @staticmethod
376425 def _switch_path (path ):
377- with suppress (Exception ):
378- return zipp .Path (path )
426+ if not PYPY_OPEN_BUG or os .path .isfile (path ): # pragma: no branch
427+ with suppress (Exception ):
428+ return zipp .Path (path )
379429 return pathlib .Path (path )
380430
381431 @classmethod
382- def _predicate (cls , pattern , root , item ):
383- return re .match (pattern , str (item .name ), flags = re .IGNORECASE )
432+ def _matches_info (cls , normalized , item ):
433+ template = r'{pattern}(-.*)?\.(dist|egg)-info'
434+ manifest = template .format (pattern = normalized )
435+ return re .match (manifest , item .name , flags = re .IGNORECASE )
436+
437+ @classmethod
438+ def _matches_legacy (cls , normalized , item ):
439+ template = r'{pattern}-.*\.egg[\\/]EGG-INFO'
440+ manifest = template .format (pattern = normalized )
441+ return re .search (manifest , str (item ), flags = re .IGNORECASE )
384442
385443 @classmethod
386444 def _search_path (cls , root , pattern ):
387445 if not root .is_dir ():
388446 return ()
389447 normalized = pattern .replace ('-' , '_' )
390- matcher = cls .search_template .format (pattern = normalized )
391448 return (item for item in root .iterdir ()
392- if cls ._predicate (matcher , root , item ))
449+ if cls ._matches_info (normalized , item )
450+ or cls ._matches_legacy (normalized , item ))
393451
394452
395453class PathDistribution (Distribution ):
396454 def __init__ (self , path ):
397- """Construct a distribution from a path to the metadata directory."""
455+ """Construct a distribution from a path to the metadata directory.
456+
457+ :param path: A pathlib.Path or similar object supporting
458+ .joinpath(), __div__, .parent, and .read_text().
459+ """
398460 self ._path = path
399461
400462 def read_text (self , filename ):
401- with suppress (FileNotFoundError , NotADirectoryError , KeyError ):
463+ with suppress (FileNotFoundError , IsADirectoryError , KeyError ,
464+ NotADirectoryError , PermissionError ):
402465 return self ._path .joinpath (filename ).read_text (encoding = 'utf-8' )
403466 read_text .__doc__ = Distribution .read_text .__doc__
404467
405468 def locate_file (self , path ):
406469 return self ._path .parent / path
407470
408471
409- def distribution (package ):
410- """Get the ``Distribution`` instance for the given package.
472+ def distribution (distribution_name ):
473+ """Get the ``Distribution`` instance for the named package.
411474
412- :param package : The name of the package as a string.
475+ :param distribution_name : The name of the distribution package as a string.
413476 :return: A ``Distribution`` instance (or subclass thereof).
414477 """
415- return Distribution .from_name (package )
478+ return Distribution .from_name (distribution_name )
416479
417480
418- def distributions ():
481+ def distributions (** kwargs ):
419482 """Get all ``Distribution`` instances in the current environment.
420483
421484 :return: An iterable of ``Distribution`` instances.
422485 """
423- return Distribution .discover ()
486+ return Distribution .discover (** kwargs )
424487
425488
426- def local_distribution ( ):
427- """Get the ``Distribution`` instance for the package in CWD .
489+ def metadata ( distribution_name ):
490+ """Get the metadata for the named package .
428491
429- :return: A ``Distribution`` instance (or subclass thereof).
430- """
431- return Distribution .find_local ()
432-
433-
434- def metadata (package ):
435- """Get the metadata for the package.
436-
437- :param package: The name of the distribution package to query.
492+ :param distribution_name: The name of the distribution package to query.
438493 :return: An email.Message containing the parsed metadata.
439494 """
440- return Distribution .from_name (package ).metadata
495+ return Distribution .from_name (distribution_name ).metadata
441496
442497
443- def version (package ):
498+ def version (distribution_name ):
444499 """Get the version string for the named package.
445500
446- :param package : The name of the distribution package to query.
501+ :param distribution_name : The name of the distribution package to query.
447502 :return: The version string for the package as defined in the package's
448503 "Version" metadata key.
449504 """
450- return distribution (package ).version
505+ return distribution (distribution_name ).version
451506
452507
453508def entry_points ():
@@ -466,18 +521,23 @@ def entry_points():
466521 }
467522
468523
469- def files (package ):
470- return distribution (package ).files
524+ def files (distribution_name ):
525+ """Return a list of files for the named package.
526+
527+ :param distribution_name: The name of the distribution package to query.
528+ :return: List of files composing the distribution.
529+ """
530+ return distribution (distribution_name ).files
471531
472532
473- def requires (package ):
533+ def requires (distribution_name ):
474534 """
475- Return a list of requirements for the indicated distribution .
535+ Return a list of requirements for the named package .
476536
477537 :return: An iterator of requirements, suitable for
478538 packaging.requirement.Requirement.
479539 """
480- return distribution (package ).requires
540+ return distribution (distribution_name ).requires
481541
482542
483543__version__ = version (__name__ )
0 commit comments