Skip to content

Commit f694cb0

Browse files
committed
Merge branch 'master' into feature/pep517-metadata
2 parents 596b2b6 + 0d8384c commit f694cb0

File tree

17 files changed

+414
-131
lines changed

17 files changed

+414
-131
lines changed

.gitlab-ci.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ image: quay.io/python-devs/ci-image
22

33
stages:
44
- test
5+
- qa
6+
- docs
57
- codecov
68
- deploy
79

@@ -11,18 +13,18 @@ qa:
1113

1214
tests:
1315
script:
14-
- tox -e py27,py34,py35,py36,py37,py38
16+
- tox -e py27,py35,py36,py37,py38
1517

1618
coverage:
1719
script:
18-
- tox -e py27-cov,py34-cov,py35-cov,py36-cov,py37-cov,py38-cov
20+
- tox -e py27-cov,py35-cov,py36-cov,py37-cov,py38-cov
1921
artifacts:
2022
paths:
2123
- coverage.xml
2224

2325
diffcov:
2426
script:
25-
- tox -e py27-diffcov,py34-diffcov,py35-diffcov,py36-diffcov,py37-diffcov,py38-diffcov
27+
- tox -e py27-diffcov,py35-diffcov,py36-diffcov,py37-diffcov,py38-diffcov
2628

2729
docs:
2830
script:

importlib_metadata/__init__.py

Lines changed: 119 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import unicode_literals, absolute_import
22

33
import io
4+
import os
45
import re
56
import abc
67
import csv
@@ -18,11 +19,15 @@
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
)
2732
from importlib import import_module
2833
from itertools import starmap
@@ -33,6 +38,7 @@
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

395453
class 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

453508
def 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

Comments
 (0)