util.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. from __future__ import annotations
  2. from operator import attrgetter
  3. from typing import TYPE_CHECKING
  4. from zipfile import ZipFile
  5. if TYPE_CHECKING:
  6. from pathlib import Path
  7. class Wheel:
  8. def __init__(self, path: Path) -> None:
  9. # https://www.python.org/dev/peps/pep-0427/#file-name-convention
  10. # The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
  11. self.path = path
  12. self._parts = path.stem.split("-")
  13. @classmethod
  14. def from_path(cls, path: Path) -> Wheel | None:
  15. if path is not None and path.suffix == ".whl" and len(path.stem.split("-")) >= 5: # noqa: PLR2004
  16. return cls(path)
  17. return None
  18. @property
  19. def distribution(self) -> str:
  20. return self._parts[0]
  21. @property
  22. def version(self) -> str:
  23. return self._parts[1]
  24. @property
  25. def version_tuple(self) -> tuple[int, ...]:
  26. return self.as_version_tuple(self.version)
  27. @staticmethod
  28. def as_version_tuple(version: str) -> tuple[int, ...]:
  29. result = []
  30. for part in version.split(".")[0:3]:
  31. try:
  32. result.append(int(part))
  33. except ValueError: # noqa: PERF203
  34. break
  35. if not result:
  36. raise ValueError(version)
  37. return tuple(result)
  38. @property
  39. def name(self) -> str:
  40. return self.path.name
  41. def support_py(self, py_version: str) -> bool:
  42. name = f"{'-'.join(self.path.stem.split('-')[0:2])}.dist-info/METADATA"
  43. with ZipFile(str(self.path), "r") as zip_file:
  44. metadata = zip_file.read(name).decode("utf-8")
  45. marker = "Requires-Python:"
  46. requires = next((i[len(marker) :] for i in metadata.splitlines() if i.startswith(marker)), None)
  47. if requires is None: # if it does not specify a python requires the assumption is compatible
  48. return True
  49. py_version_int = tuple(int(i) for i in py_version.split("."))
  50. for require in (i.strip() for i in requires.split(",")):
  51. # https://www.python.org/dev/peps/pep-0345/#version-specifiers
  52. for operator, check in [
  53. ("!=", lambda v: py_version_int != v),
  54. ("==", lambda v: py_version_int == v),
  55. ("<=", lambda v: py_version_int <= v),
  56. (">=", lambda v: py_version_int >= v),
  57. ("<", lambda v: py_version_int < v),
  58. (">", lambda v: py_version_int > v),
  59. ]:
  60. if require.startswith(operator):
  61. ver_str = require[len(operator) :].strip()
  62. version = tuple((int(i) if i != "*" else None) for i in ver_str.split("."))[0:2]
  63. if not check(version):
  64. return False
  65. break
  66. return True
  67. def __repr__(self) -> str:
  68. return f"{self.__class__.__name__}({self.path})"
  69. def __str__(self) -> str:
  70. return str(self.path)
  71. def discover_wheels(from_folder: Path, distribution: str, version: str | None, for_py_version: str) -> list[Wheel]:
  72. wheels = []
  73. for filename in from_folder.iterdir():
  74. wheel = Wheel.from_path(filename)
  75. if (
  76. wheel
  77. and wheel.distribution == distribution
  78. and (version is None or wheel.version == version)
  79. and wheel.support_py(for_py_version)
  80. ):
  81. wheels.append(wheel)
  82. return sorted(wheels, key=attrgetter("version_tuple", "distribution"), reverse=True)
  83. class Version:
  84. #: the version bundled with virtualenv
  85. bundle = "bundle"
  86. embed = "embed"
  87. #: custom version handlers
  88. non_version = (bundle, embed)
  89. @staticmethod
  90. def of_version(value: str | None) -> str | None:
  91. return None if value in Version.non_version else value
  92. @staticmethod
  93. def as_pip_req(distribution: str, version: str | None) -> str:
  94. return f"{distribution}{Version.as_version_spec(version)}"
  95. @staticmethod
  96. def as_version_spec(version: str | None) -> str:
  97. of_version = Version.of_version(version)
  98. return "" if of_version is None else f"=={of_version}"
  99. __all__ = [
  100. "Version",
  101. "Wheel",
  102. "discover_wheels",
  103. ]