_hooks.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from __future__ import unicode_literals, absolute_import
  2. import re
  3. import sys
  4. import zipp
  5. import itertools
  6. from .api import Distribution
  7. if sys.version_info >= (3,): # pragma: nocover
  8. from contextlib import suppress
  9. from pathlib import Path
  10. else: # pragma: nocover
  11. from contextlib2 import suppress # noqa
  12. from itertools import imap as map # type: ignore
  13. from pathlib2 import Path
  14. FileNotFoundError = IOError, OSError
  15. __metaclass__ = type
  16. def install(cls):
  17. """Class decorator for installation on sys.meta_path."""
  18. sys.meta_path.append(cls)
  19. return cls
  20. class NullFinder:
  21. @staticmethod
  22. def find_spec(*args, **kwargs):
  23. return None
  24. # In Python 2, the import system requires finders
  25. # to have a find_module() method, but this usage
  26. # is deprecated in Python 3 in favor of find_spec().
  27. # For the purposes of this finder (i.e. being present
  28. # on sys.meta_path but having no other import
  29. # system functionality), the two methods are identical.
  30. find_module = find_spec
  31. @install
  32. class MetadataPathFinder(NullFinder):
  33. """A degenerate finder for distribution packages on the file system.
  34. This finder supplies only a find_distributions() method for versions
  35. of Python that do not have a PathFinder find_distributions().
  36. """
  37. search_template = r'{pattern}(-.*)?\.(dist|egg)-info'
  38. @classmethod
  39. def find_distributions(cls, name=None, path=None):
  40. """Return an iterable of all Distribution instances capable of
  41. loading the metadata for packages matching the name
  42. (or all names if not supplied) along the paths in the list
  43. of directories ``path`` (defaults to sys.path).
  44. """
  45. if path is None:
  46. path = sys.path
  47. pattern = '.*' if name is None else re.escape(name)
  48. found = cls._search_paths(pattern, path)
  49. return map(PathDistribution, found)
  50. @classmethod
  51. def _search_paths(cls, pattern, paths):
  52. """
  53. Find metadata directories in paths heuristically.
  54. """
  55. return itertools.chain.from_iterable(
  56. cls._search_path(path, pattern)
  57. for path in map(Path, paths)
  58. )
  59. @classmethod
  60. def _search_path(cls, root, pattern):
  61. if not root.is_dir():
  62. return ()
  63. normalized = pattern.replace('-', '_')
  64. return (
  65. item
  66. for item in root.iterdir()
  67. if item.is_dir()
  68. and re.match(
  69. cls.search_template.format(pattern=normalized),
  70. str(item.name),
  71. flags=re.IGNORECASE,
  72. )
  73. )
  74. class PathDistribution(Distribution):
  75. def __init__(self, path):
  76. """Construct a distribution from a path to the metadata directory."""
  77. self._path = path
  78. def read_text(self, filename):
  79. with suppress(FileNotFoundError):
  80. with self._path.joinpath(filename).open(encoding='utf-8') as fp:
  81. return fp.read()
  82. return None
  83. read_text.__doc__ = Distribution.read_text.__doc__
  84. def locate_file(self, path):
  85. return self._path.parent / path
  86. @install
  87. class WheelMetadataFinder(NullFinder):
  88. """A degenerate finder for distribution packages in wheels.
  89. This finder supplies only a find_distributions() method for versions
  90. of Python that do not have a PathFinder find_distributions().
  91. """
  92. search_template = r'{pattern}(-.*)?\.whl'
  93. @classmethod
  94. def find_distributions(cls, name=None, path=None):
  95. """Return an iterable of all Distribution instances capable of
  96. loading the metadata for packages matching the name
  97. (or all names if not supplied) along the paths in the list
  98. of directories ``path`` (defaults to sys.path).
  99. """
  100. if path is None:
  101. path = sys.path
  102. pattern = '.*' if name is None else re.escape(name)
  103. found = cls._search_paths(pattern, path)
  104. return map(WheelDistribution, found)
  105. @classmethod
  106. def _search_paths(cls, pattern, paths):
  107. return (
  108. path
  109. for path in map(Path, paths)
  110. if re.match(
  111. cls.search_template.format(pattern=pattern),
  112. str(path.name),
  113. flags=re.IGNORECASE,
  114. )
  115. )
  116. class WheelDistribution(Distribution):
  117. def __init__(self, archive):
  118. self._archive = zipp.Path(archive)
  119. name, version = archive.name.split('-')[0:2]
  120. self._dist_info = '{}-{}.dist-info'.format(name, version)
  121. def read_text(self, filename):
  122. target = self._archive / self._dist_info / filename
  123. return target.read_text() if target.exists() else None
  124. read_text.__doc__ = Distribution.read_text.__doc__
  125. def locate_file(self, path):
  126. return self._archive / path