utils.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from __future__ import annotations
  2. import errno
  3. import hashlib
  4. import os
  5. import re
  6. import sys
  7. from functools import lru_cache
  8. from pathlib import Path
  9. from typing import TYPE_CHECKING, cast
  10. if TYPE_CHECKING:
  11. from typing import Generator, Sequence, TypedDict
  12. VERSION_RE = re.compile(
  13. r"(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>[0-9]+))?)?\.?"
  14. r"(?:(?P<prerel>[abc]|rc|dev)(?:(?P<prerelversion>\d+(?:\.\d+)*))?)"
  15. r"?(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?"
  16. r"(?:-(?P<architecture>32|64))?"
  17. )
  18. WINDOWS = sys.platform == "win32"
  19. MACOS = sys.platform == "darwin"
  20. PYTHON_IMPLEMENTATIONS = (
  21. "python",
  22. "ironpython",
  23. "jython",
  24. "pypy",
  25. "anaconda",
  26. "miniconda",
  27. "stackless",
  28. "activepython",
  29. "pyston",
  30. "micropython",
  31. )
  32. if WINDOWS:
  33. KNOWN_EXTS: Sequence[str] = (".exe", "", ".py", ".bat")
  34. else:
  35. KNOWN_EXTS = ("", ".sh", ".bash", ".csh", ".zsh", ".fish", ".py")
  36. PY_MATCH_STR = (
  37. r"((?P<implementation>{0})(?:\d(?:\.?\d\d?[cpm]{{0,3}})?)?"
  38. r"(?:(?<=\d)-[\d\.]+)*(?!w))(?P<suffix>{1})$".format(
  39. "|".join(PYTHON_IMPLEMENTATIONS),
  40. "|".join(KNOWN_EXTS),
  41. )
  42. )
  43. RE_MATCHER = re.compile(PY_MATCH_STR)
  44. def safe_iter_dir(path: Path) -> Generator[Path, None, None]:
  45. """Iterate over a directory, returning an empty iterator if the path
  46. is not a directory or is not readable.
  47. """
  48. if not os.access(str(path), os.R_OK) or not path.is_dir():
  49. return
  50. try:
  51. yield from path.iterdir()
  52. except OSError as exc:
  53. if exc.errno == errno.EACCES:
  54. return
  55. raise
  56. @lru_cache(maxsize=1024)
  57. def path_is_known_executable(path: Path) -> bool:
  58. """
  59. Returns whether a given path is a known executable from known executable extensions
  60. or has the executable bit toggled.
  61. :param path: The path to the target executable.
  62. :type path: :class:`~Path`
  63. :return: True if the path has chmod +x, or is a readable, known executable extension.
  64. :rtype: bool
  65. """
  66. try:
  67. return (
  68. path.is_file()
  69. and os.access(str(path), os.R_OK)
  70. and (path.suffix in KNOWN_EXTS or os.access(str(path), os.X_OK))
  71. )
  72. except OSError:
  73. return False
  74. @lru_cache(maxsize=1024)
  75. def looks_like_python(name: str) -> bool:
  76. """
  77. Determine whether the supplied filename looks like a possible name of python.
  78. :param str name: The name of the provided file.
  79. :return: Whether the provided name looks like python.
  80. :rtype: bool
  81. """
  82. if not any(name.lower().startswith(py_name) for py_name in PYTHON_IMPLEMENTATIONS):
  83. return False
  84. match = RE_MATCHER.match(name)
  85. return bool(match)
  86. @lru_cache(maxsize=1024)
  87. def path_is_python(path: Path) -> bool:
  88. """
  89. Determine whether the supplied path is a executable and looks like
  90. a possible path to python.
  91. :param path: The path to an executable.
  92. :type path: :class:`~Path`
  93. :return: Whether the provided path is an executable path to python.
  94. :rtype: bool
  95. """
  96. return looks_like_python(path.name) and path_is_known_executable(path)
  97. @lru_cache(maxsize=1024)
  98. def get_binary_hash(path: Path) -> str:
  99. """Return the MD5 hash of the given file."""
  100. hasher = hashlib.md5()
  101. with path.open("rb") as f:
  102. for chunk in iter(lambda: f.read(4096), b""):
  103. hasher.update(chunk)
  104. return hasher.hexdigest()
  105. if TYPE_CHECKING:
  106. class VersionDict(TypedDict):
  107. pre: bool | None
  108. dev: bool | None
  109. major: int | None
  110. minor: int | None
  111. patch: int | None
  112. architecture: str | None
  113. def parse_major(version: str) -> VersionDict | None:
  114. """Parse the version dict from the version string"""
  115. match = VERSION_RE.match(version)
  116. if not match:
  117. return None
  118. rv = match.groupdict()
  119. rv["pre"] = bool(rv.pop("prerel"))
  120. rv["dev"] = bool(rv.pop("dev"))
  121. for int_values in ("major", "minor", "patch"):
  122. if rv[int_values] is not None:
  123. rv[int_values] = int(rv[int_values])
  124. if rv["architecture"]:
  125. rv["architecture"] = f"{rv['architecture']}bit"
  126. return cast("VersionDict", rv)
  127. def get_suffix_preference(name: str) -> int:
  128. for i, suffix in enumerate(KNOWN_EXTS):
  129. if suffix and name.endswith(suffix):
  130. return i
  131. return KNOWN_EXTS.index("")