wheel.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """Represents a wheel file and provides access to the various parts of the
  2. name that have meaning.
  3. """
  4. from __future__ import annotations
  5. import re
  6. from collections.abc import Iterable
  7. from pip._vendor.packaging.tags import Tag
  8. from pip._vendor.packaging.utils import BuildTag, parse_wheel_filename
  9. from pip._vendor.packaging.utils import (
  10. InvalidWheelFilename as _PackagingInvalidWheelFilename,
  11. )
  12. from pip._internal.exceptions import InvalidWheelFilename
  13. from pip._internal.utils.deprecation import deprecated
  14. class Wheel:
  15. """A wheel file"""
  16. legacy_wheel_file_re = re.compile(
  17. r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
  18. ((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?)
  19. \.whl|\.dist-info)$""",
  20. re.VERBOSE,
  21. )
  22. def __init__(self, filename: str) -> None:
  23. self.filename = filename
  24. # To make mypy happy specify type hints that can come from either
  25. # parse_wheel_filename or the legacy_wheel_file_re match.
  26. self.name: str
  27. self._build_tag: BuildTag | None = None
  28. try:
  29. wheel_info = parse_wheel_filename(filename)
  30. self.name, _version, self._build_tag, self.file_tags = wheel_info
  31. self.version = str(_version)
  32. except _PackagingInvalidWheelFilename as e:
  33. # Check if the wheel filename is in the legacy format
  34. legacy_wheel_info = self.legacy_wheel_file_re.match(filename)
  35. if not legacy_wheel_info:
  36. raise InvalidWheelFilename(e.args[0]) from None
  37. deprecated(
  38. reason=(
  39. f"Wheel filename {filename!r} is not correctly normalised. "
  40. "Future versions of pip will raise the following error:\n"
  41. f"{e.args[0]}\n\n"
  42. ),
  43. replacement=(
  44. "to rename the wheel to use a correctly normalised "
  45. "name (this may require updating the version in "
  46. "the project metadata)"
  47. ),
  48. gone_in="25.3",
  49. issue=12938,
  50. )
  51. self.name = legacy_wheel_info.group("name").replace("_", "-")
  52. self.version = legacy_wheel_info.group("ver").replace("_", "-")
  53. # Generate the file tags from the legacy wheel filename
  54. pyversions = legacy_wheel_info.group("pyver").split(".")
  55. abis = legacy_wheel_info.group("abi").split(".")
  56. plats = legacy_wheel_info.group("plat").split(".")
  57. self.file_tags = frozenset(
  58. Tag(interpreter=py, abi=abi, platform=plat)
  59. for py in pyversions
  60. for abi in abis
  61. for plat in plats
  62. )
  63. @property
  64. def build_tag(self) -> BuildTag:
  65. if self._build_tag is not None:
  66. return self._build_tag
  67. # Parse the build tag from the legacy wheel filename
  68. legacy_wheel_info = self.legacy_wheel_file_re.match(self.filename)
  69. assert legacy_wheel_info is not None, "guaranteed by filename validation"
  70. build_tag = legacy_wheel_info.group("build")
  71. match = re.match(r"^(\d+)(.*)$", build_tag)
  72. assert match is not None, "guaranteed by filename validation"
  73. build_tag_groups = match.groups()
  74. self._build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
  75. return self._build_tag
  76. def get_formatted_file_tags(self) -> list[str]:
  77. """Return the wheel's tags as a sorted list of strings."""
  78. return sorted(str(tag) for tag in self.file_tags)
  79. def support_index_min(self, tags: list[Tag]) -> int:
  80. """Return the lowest index that one of the wheel's file_tag combinations
  81. achieves in the given list of supported tags.
  82. For example, if there are 8 supported tags and one of the file tags
  83. is first in the list, then return 0.
  84. :param tags: the PEP 425 tags to check the wheel against, in order
  85. with most preferred first.
  86. :raises ValueError: If none of the wheel's file tags match one of
  87. the supported tags.
  88. """
  89. try:
  90. return next(i for i, t in enumerate(tags) if t in self.file_tags)
  91. except StopIteration:
  92. raise ValueError()
  93. def find_most_preferred_tag(
  94. self, tags: list[Tag], tag_to_priority: dict[Tag, int]
  95. ) -> int:
  96. """Return the priority of the most preferred tag that one of the wheel's file
  97. tag combinations achieves in the given list of supported tags using the given
  98. tag_to_priority mapping, where lower priorities are more-preferred.
  99. This is used in place of support_index_min in some cases in order to avoid
  100. an expensive linear scan of a large list of tags.
  101. :param tags: the PEP 425 tags to check the wheel against.
  102. :param tag_to_priority: a mapping from tag to priority of that tag, where
  103. lower is more preferred.
  104. :raises ValueError: If none of the wheel's file tags match one of
  105. the supported tags.
  106. """
  107. return min(
  108. tag_to_priority[tag] for tag in self.file_tags if tag in tag_to_priority
  109. )
  110. def supported(self, tags: Iterable[Tag]) -> bool:
  111. """Return whether the wheel is compatible with one of the given tags.
  112. :param tags: the PEP 425 tags to check the wheel against.
  113. """
  114. return not self.file_tags.isdisjoint(tags)