wheel.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Support functions for working with wheel files.
  2. """
  3. from __future__ import absolute_import
  4. import logging
  5. from email.parser import Parser
  6. from zipfile import ZipFile
  7. from pip._vendor.packaging.utils import canonicalize_name
  8. from pip._vendor.pkg_resources import DistInfoDistribution
  9. from pip._vendor.six import PY2, ensure_str
  10. from pip._internal.exceptions import UnsupportedWheel
  11. from pip._internal.utils.pkg_resources import DictMetadata
  12. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  13. if MYPY_CHECK_RUNNING:
  14. from email.message import Message
  15. from typing import Dict, Tuple
  16. from pip._vendor.pkg_resources import Distribution
  17. if PY2:
  18. from zipfile import BadZipfile as BadZipFile
  19. else:
  20. from zipfile import BadZipFile
  21. VERSION_COMPATIBLE = (1, 0)
  22. logger = logging.getLogger(__name__)
  23. class WheelMetadata(DictMetadata):
  24. """Metadata provider that maps metadata decoding exceptions to our
  25. internal exception type.
  26. """
  27. def __init__(self, metadata, wheel_name):
  28. # type: (Dict[str, bytes], str) -> None
  29. super(WheelMetadata, self).__init__(metadata)
  30. self._wheel_name = wheel_name
  31. def get_metadata(self, name):
  32. # type: (str) -> str
  33. try:
  34. return super(WheelMetadata, self).get_metadata(name)
  35. except UnicodeDecodeError as e:
  36. # Augment the default error with the origin of the file.
  37. raise UnsupportedWheel(
  38. "Error decoding metadata for {}: {}".format(
  39. self._wheel_name, e
  40. )
  41. )
  42. def pkg_resources_distribution_for_wheel(wheel_zip, name, location):
  43. # type: (ZipFile, str, str) -> Distribution
  44. """Get a pkg_resources distribution given a wheel.
  45. :raises UnsupportedWheel: on any errors
  46. """
  47. info_dir, _ = parse_wheel(wheel_zip, name)
  48. metadata_files = [
  49. p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir))
  50. ]
  51. metadata_text = {} # type: Dict[str, bytes]
  52. for path in metadata_files:
  53. # If a flag is set, namelist entries may be unicode in Python 2.
  54. # We coerce them to native str type to match the types used in the rest
  55. # of the code. This cannot fail because unicode can always be encoded
  56. # with UTF-8.
  57. full_path = ensure_str(path)
  58. _, metadata_name = full_path.split("/", 1)
  59. try:
  60. metadata_text[metadata_name] = read_wheel_metadata_file(
  61. wheel_zip, full_path
  62. )
  63. except UnsupportedWheel as e:
  64. raise UnsupportedWheel(
  65. "{} has an invalid wheel, {}".format(name, str(e))
  66. )
  67. metadata = WheelMetadata(metadata_text, location)
  68. return DistInfoDistribution(
  69. location=location, metadata=metadata, project_name=name
  70. )
  71. def parse_wheel(wheel_zip, name):
  72. # type: (ZipFile, str) -> Tuple[str, Message]
  73. """Extract information from the provided wheel, ensuring it meets basic
  74. standards.
  75. Returns the name of the .dist-info directory and the parsed WHEEL metadata.
  76. """
  77. try:
  78. info_dir = wheel_dist_info_dir(wheel_zip, name)
  79. metadata = wheel_metadata(wheel_zip, info_dir)
  80. version = wheel_version(metadata)
  81. except UnsupportedWheel as e:
  82. raise UnsupportedWheel(
  83. "{} has an invalid wheel, {}".format(name, str(e))
  84. )
  85. check_compatibility(version, name)
  86. return info_dir, metadata
  87. def wheel_dist_info_dir(source, name):
  88. # type: (ZipFile, str) -> str
  89. """Returns the name of the contained .dist-info directory.
  90. Raises AssertionError or UnsupportedWheel if not found, >1 found, or
  91. it doesn't match the provided name.
  92. """
  93. # Zip file path separators must be /
  94. subdirs = list(set(p.split("/")[0] for p in source.namelist()))
  95. info_dirs = [s for s in subdirs if s.endswith('.dist-info')]
  96. if not info_dirs:
  97. raise UnsupportedWheel(".dist-info directory not found")
  98. if len(info_dirs) > 1:
  99. raise UnsupportedWheel(
  100. "multiple .dist-info directories found: {}".format(
  101. ", ".join(info_dirs)
  102. )
  103. )
  104. info_dir = info_dirs[0]
  105. info_dir_name = canonicalize_name(info_dir)
  106. canonical_name = canonicalize_name(name)
  107. if not info_dir_name.startswith(canonical_name):
  108. raise UnsupportedWheel(
  109. ".dist-info directory {!r} does not start with {!r}".format(
  110. info_dir, canonical_name
  111. )
  112. )
  113. # Zip file paths can be unicode or str depending on the zip entry flags,
  114. # so normalize it.
  115. return ensure_str(info_dir)
  116. def read_wheel_metadata_file(source, path):
  117. # type: (ZipFile, str) -> bytes
  118. try:
  119. return source.read(path)
  120. # BadZipFile for general corruption, KeyError for missing entry,
  121. # and RuntimeError for password-protected files
  122. except (BadZipFile, KeyError, RuntimeError) as e:
  123. raise UnsupportedWheel(
  124. "could not read {!r} file: {!r}".format(path, e)
  125. )
  126. def wheel_metadata(source, dist_info_dir):
  127. # type: (ZipFile, str) -> Message
  128. """Return the WHEEL metadata of an extracted wheel, if possible.
  129. Otherwise, raise UnsupportedWheel.
  130. """
  131. path = "{}/WHEEL".format(dist_info_dir)
  132. # Zip file path separators must be /
  133. wheel_contents = read_wheel_metadata_file(source, path)
  134. try:
  135. wheel_text = ensure_str(wheel_contents)
  136. except UnicodeDecodeError as e:
  137. raise UnsupportedWheel("error decoding {!r}: {!r}".format(path, e))
  138. # FeedParser (used by Parser) does not raise any exceptions. The returned
  139. # message may have .defects populated, but for backwards-compatibility we
  140. # currently ignore them.
  141. return Parser().parsestr(wheel_text)
  142. def wheel_version(wheel_data):
  143. # type: (Message) -> Tuple[int, ...]
  144. """Given WHEEL metadata, return the parsed Wheel-Version.
  145. Otherwise, raise UnsupportedWheel.
  146. """
  147. version_text = wheel_data["Wheel-Version"]
  148. if version_text is None:
  149. raise UnsupportedWheel("WHEEL is missing Wheel-Version")
  150. version = version_text.strip()
  151. try:
  152. return tuple(map(int, version.split('.')))
  153. except ValueError:
  154. raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version))
  155. def check_compatibility(version, name):
  156. # type: (Tuple[int, ...], str) -> None
  157. """Raises errors or warns if called with an incompatible Wheel-Version.
  158. pip should refuse to install a Wheel-Version that's a major series
  159. ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when
  160. installing a version only minor version ahead (e.g 1.2 > 1.1).
  161. version: a 2-tuple representing a Wheel-Version (Major, Minor)
  162. name: name of wheel or package to raise exception about
  163. :raises UnsupportedWheel: when an incompatible Wheel-Version is given
  164. """
  165. if version[0] > VERSION_COMPATIBLE[0]:
  166. raise UnsupportedWheel(
  167. "{}'s Wheel-Version ({}) is not compatible with this version "
  168. "of pip".format(name, '.'.join(map(str, version)))
  169. )
  170. elif version > VERSION_COMPATIBLE:
  171. logger.warning(
  172. 'Installing from a newer Wheel-Version (%s)',
  173. '.'.join(map(str, version)),
  174. )