utils.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. # This file is dual licensed under the terms of the Apache License, Version
  2. # 2.0, and the BSD License. See the LICENSE file in the root of this repository
  3. # for complete details.
  4. from __future__ import absolute_import, division, print_function
  5. import re
  6. from ._typing import TYPE_CHECKING, cast
  7. from .tags import Tag, parse_tag
  8. from .version import InvalidVersion, Version
  9. if TYPE_CHECKING: # pragma: no cover
  10. from typing import FrozenSet, NewType, Tuple, Union
  11. BuildTag = Union[Tuple[()], Tuple[int, str]]
  12. NormalizedName = NewType("NormalizedName", str)
  13. else:
  14. BuildTag = tuple
  15. NormalizedName = str
  16. class InvalidWheelFilename(ValueError):
  17. """
  18. An invalid wheel filename was found, users should refer to PEP 427.
  19. """
  20. class InvalidSdistFilename(ValueError):
  21. """
  22. An invalid sdist filename was found, users should refer to the packaging user guide.
  23. """
  24. _canonicalize_regex = re.compile(r"[-_.]+")
  25. # PEP 427: The build number must start with a digit.
  26. _build_tag_regex = re.compile(r"(\d+)(.*)")
  27. def canonicalize_name(name):
  28. # type: (str) -> NormalizedName
  29. # This is taken from PEP 503.
  30. value = _canonicalize_regex.sub("-", name).lower()
  31. return cast(NormalizedName, value)
  32. def canonicalize_version(version):
  33. # type: (Union[Version, str]) -> Union[Version, str]
  34. """
  35. This is very similar to Version.__str__, but has one subtle difference
  36. with the way it handles the release segment.
  37. """
  38. if not isinstance(version, Version):
  39. try:
  40. version = Version(version)
  41. except InvalidVersion:
  42. # Legacy versions cannot be normalized
  43. return version
  44. parts = []
  45. # Epoch
  46. if version.epoch != 0:
  47. parts.append("{0}!".format(version.epoch))
  48. # Release segment
  49. # NB: This strips trailing '.0's to normalize
  50. parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release)))
  51. # Pre-release
  52. if version.pre is not None:
  53. parts.append("".join(str(x) for x in version.pre))
  54. # Post-release
  55. if version.post is not None:
  56. parts.append(".post{0}".format(version.post))
  57. # Development release
  58. if version.dev is not None:
  59. parts.append(".dev{0}".format(version.dev))
  60. # Local version segment
  61. if version.local is not None:
  62. parts.append("+{0}".format(version.local))
  63. return "".join(parts)
  64. def parse_wheel_filename(filename):
  65. # type: (str) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]
  66. if not filename.endswith(".whl"):
  67. raise InvalidWheelFilename(
  68. "Invalid wheel filename (extension must be '.whl'): {0}".format(filename)
  69. )
  70. filename = filename[:-4]
  71. dashes = filename.count("-")
  72. if dashes not in (4, 5):
  73. raise InvalidWheelFilename(
  74. "Invalid wheel filename (wrong number of parts): {0}".format(filename)
  75. )
  76. parts = filename.split("-", dashes - 2)
  77. name_part = parts[0]
  78. # See PEP 427 for the rules on escaping the project name
  79. if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
  80. raise InvalidWheelFilename("Invalid project name: {0}".format(filename))
  81. name = canonicalize_name(name_part)
  82. version = Version(parts[1])
  83. if dashes == 5:
  84. build_part = parts[2]
  85. build_match = _build_tag_regex.match(build_part)
  86. if build_match is None:
  87. raise InvalidWheelFilename(
  88. "Invalid build number: {0} in '{1}'".format(build_part, filename)
  89. )
  90. build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
  91. else:
  92. build = ()
  93. tags = parse_tag(parts[-1])
  94. return (name, version, build, tags)
  95. def parse_sdist_filename(filename):
  96. # type: (str) -> Tuple[NormalizedName, Version]
  97. if not filename.endswith(".tar.gz"):
  98. raise InvalidSdistFilename(
  99. "Invalid sdist filename (extension must be '.tar.gz'): {0}".format(filename)
  100. )
  101. # We are requiring a PEP 440 version, which cannot contain dashes,
  102. # so we split on the last dash.
  103. name_part, sep, version_part = filename[:-7].rpartition("-")
  104. if not sep:
  105. raise InvalidSdistFilename("Invalid sdist filename: {0}".format(filename))
  106. name = canonicalize_name(name_part)
  107. version = Version(version_part)
  108. return (name, version)