utils.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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. import re
  5. from typing import FrozenSet, NewType, Tuple, Union, cast
  6. from .tags import Tag, parse_tag
  7. from .version import InvalidVersion, Version
  8. BuildTag = Union[Tuple[()], Tuple[int, str]]
  9. NormalizedName = NewType("NormalizedName", str)
  10. class InvalidWheelFilename(ValueError):
  11. """
  12. An invalid wheel filename was found, users should refer to PEP 427.
  13. """
  14. class InvalidSdistFilename(ValueError):
  15. """
  16. An invalid sdist filename was found, users should refer to the packaging user guide.
  17. """
  18. _canonicalize_regex = re.compile(r"[-_.]+")
  19. # PEP 427: The build number must start with a digit.
  20. _build_tag_regex = re.compile(r"(\d+)(.*)")
  21. def canonicalize_name(name: str) -> NormalizedName:
  22. # This is taken from PEP 503.
  23. value = _canonicalize_regex.sub("-", name).lower()
  24. return cast(NormalizedName, value)
  25. def canonicalize_version(
  26. version: Union[Version, str], *, strip_trailing_zero: bool = True
  27. ) -> str:
  28. """
  29. This is very similar to Version.__str__, but has one subtle difference
  30. with the way it handles the release segment.
  31. """
  32. if isinstance(version, str):
  33. try:
  34. parsed = Version(version)
  35. except InvalidVersion:
  36. # Legacy versions cannot be normalized
  37. return version
  38. else:
  39. parsed = version
  40. parts = []
  41. # Epoch
  42. if parsed.epoch != 0:
  43. parts.append(f"{parsed.epoch}!")
  44. # Release segment
  45. release_segment = ".".join(str(x) for x in parsed.release)
  46. if strip_trailing_zero:
  47. # NB: This strips trailing '.0's to normalize
  48. release_segment = re.sub(r"(\.0)+$", "", release_segment)
  49. parts.append(release_segment)
  50. # Pre-release
  51. if parsed.pre is not None:
  52. parts.append("".join(str(x) for x in parsed.pre))
  53. # Post-release
  54. if parsed.post is not None:
  55. parts.append(f".post{parsed.post}")
  56. # Development release
  57. if parsed.dev is not None:
  58. parts.append(f".dev{parsed.dev}")
  59. # Local version segment
  60. if parsed.local is not None:
  61. parts.append(f"+{parsed.local}")
  62. return "".join(parts)
  63. def parse_wheel_filename(
  64. filename: str,
  65. ) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
  66. if not filename.endswith(".whl"):
  67. raise InvalidWheelFilename(
  68. f"Invalid wheel filename (extension must be '.whl'): {filename}"
  69. )
  70. filename = filename[:-4]
  71. dashes = filename.count("-")
  72. if dashes not in (4, 5):
  73. raise InvalidWheelFilename(
  74. f"Invalid wheel filename (wrong number of parts): {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(f"Invalid project name: {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. f"Invalid build number: {build_part} in '{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: str) -> Tuple[NormalizedName, Version]:
  96. if filename.endswith(".tar.gz"):
  97. file_stem = filename[: -len(".tar.gz")]
  98. elif filename.endswith(".zip"):
  99. file_stem = filename[: -len(".zip")]
  100. else:
  101. raise InvalidSdistFilename(
  102. f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
  103. f" {filename}"
  104. )
  105. # We are requiring a PEP 440 version, which cannot contain dashes,
  106. # so we split on the last dash.
  107. name_part, sep, version_part = file_stem.rpartition("-")
  108. if not sep:
  109. raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
  110. name = canonicalize_name(name_part)
  111. version = Version(version_part)
  112. return (name, version)