util.py 3.9 KB

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