|
1 | | -from __future__ import absolute_import |
| 1 | +from __future__ import unicode_literals, absolute_import |
2 | 2 |
|
3 | 3 | import io |
4 | 4 | import re |
5 | 5 | import abc |
6 | 6 | import csv |
7 | 7 | import sys |
| 8 | +import zipp |
8 | 9 | import email |
9 | 10 | import operator |
10 | 11 | import functools |
|
13 | 14 |
|
14 | 15 | from importlib import import_module |
15 | 16 | from itertools import starmap |
| 17 | +from ._hooks import install, NullFinder |
16 | 18 |
|
17 | 19 | if sys.version_info > (3,): # pragma: nocover |
18 | 20 | from configparser import ConfigParser |
| 21 | + from contextlib import suppress |
19 | 22 | else: # pragma: nocover |
20 | 23 | from backports.configparser import ConfigParser |
21 | 24 | from itertools import imap as map # type: ignore |
| 25 | + from contextlib2 import suppress # noqa |
| 26 | + FileNotFoundError = IOError, OSError |
| 27 | + NotADirectoryError = IOError, OSError |
22 | 28 |
|
23 | 29 | if sys.version_info > (3, 5): # pragma: nocover |
24 | 30 | import pathlib |
|
31 | 37 | BaseClass = ImportError # type: ignore |
32 | 38 |
|
33 | 39 |
|
| 40 | +if sys.version_info >= (3,): # pragma: nocover |
| 41 | + from importlib.abc import MetaPathFinder |
| 42 | +else: # pragma: nocover |
| 43 | + class MetaPathFinder(object): |
| 44 | + __metaclass__ = abc.ABCMeta |
| 45 | + |
| 46 | + |
34 | 47 | __metaclass__ = type |
35 | 48 |
|
36 | 49 |
|
@@ -305,6 +318,86 @@ def parse_condition(section): |
305 | 318 | yield dep + parse_condition(section) |
306 | 319 |
|
307 | 320 |
|
| 321 | +class DistributionFinder(MetaPathFinder): |
| 322 | + """ |
| 323 | + A MetaPathFinder capable of discovering installed distributions. |
| 324 | + """ |
| 325 | + |
| 326 | + @abc.abstractmethod |
| 327 | + def find_distributions(self, name=None, path=None): |
| 328 | + """ |
| 329 | + Return an iterable of all Distribution instances capable of |
| 330 | + loading the metadata for packages matching the name |
| 331 | + (or all names if not supplied) along the paths in the list |
| 332 | + of directories ``path`` (defaults to sys.path). |
| 333 | + """ |
| 334 | + |
| 335 | + |
| 336 | +@install |
| 337 | +class MetadataPathFinder(NullFinder, DistributionFinder): |
| 338 | + """A degenerate finder for distribution packages on the file system. |
| 339 | +
|
| 340 | + This finder supplies only a find_distributions() method for versions |
| 341 | + of Python that do not have a PathFinder find_distributions(). |
| 342 | + """ |
| 343 | + search_template = r'{pattern}(-.*)?\.(dist|egg)-info' |
| 344 | + |
| 345 | + def find_distributions(self, name=None, path=None): |
| 346 | + """Return an iterable of all Distribution instances capable of |
| 347 | + loading the metadata for packages matching the name |
| 348 | + (or all names if not supplied) along the paths in the list |
| 349 | + of directories ``path`` (defaults to sys.path). |
| 350 | + """ |
| 351 | + if path is None: |
| 352 | + path = sys.path |
| 353 | + pattern = '.*' if name is None else re.escape(name) |
| 354 | + found = self._search_paths(pattern, path) |
| 355 | + return map(PathDistribution, found) |
| 356 | + |
| 357 | + @classmethod |
| 358 | + def _search_paths(cls, pattern, paths): |
| 359 | + """ |
| 360 | + Find metadata directories in paths heuristically. |
| 361 | + """ |
| 362 | + return itertools.chain.from_iterable( |
| 363 | + cls._search_path(path, pattern) |
| 364 | + for path in map(cls._switch_path, paths) |
| 365 | + ) |
| 366 | + |
| 367 | + @staticmethod |
| 368 | + def _switch_path(path): |
| 369 | + with suppress(Exception): |
| 370 | + return zipp.Path(path) |
| 371 | + return pathlib.Path(path) |
| 372 | + |
| 373 | + @classmethod |
| 374 | + def _predicate(cls, pattern, root, item): |
| 375 | + return re.match(pattern, str(item.name), flags=re.IGNORECASE) |
| 376 | + |
| 377 | + @classmethod |
| 378 | + def _search_path(cls, root, pattern): |
| 379 | + if not root.is_dir(): |
| 380 | + return () |
| 381 | + normalized = pattern.replace('-', '_') |
| 382 | + matcher = cls.search_template.format(pattern=normalized) |
| 383 | + return (item for item in root.iterdir() |
| 384 | + if cls._predicate(matcher, root, item)) |
| 385 | + |
| 386 | + |
| 387 | +class PathDistribution(Distribution): |
| 388 | + def __init__(self, path): |
| 389 | + """Construct a distribution from a path to the metadata directory.""" |
| 390 | + self._path = path |
| 391 | + |
| 392 | + def read_text(self, filename): |
| 393 | + with suppress(FileNotFoundError, NotADirectoryError, KeyError): |
| 394 | + return self._path.joinpath(filename).read_text(encoding='utf-8') |
| 395 | + read_text.__doc__ = Distribution.read_text.__doc__ |
| 396 | + |
| 397 | + def locate_file(self, path): |
| 398 | + return self._path.parent / path |
| 399 | + |
| 400 | + |
308 | 401 | def _email_message_from_string(text): |
309 | 402 | # Work around https://bugs.python.org/issue25545 where |
310 | 403 | # email.message_from_string cannot handle Unicode on Python 2. |
|
0 commit comments