m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
8.2 KiB

6 months ago
  1. import datetime
  2. import functools
  3. import hashlib
  4. import json
  5. import logging
  6. import optparse
  7. import os.path
  8. import sys
  9. from dataclasses import dataclass
  10. from typing import Any, Callable, Dict, Optional
  11. from pip._vendor.packaging.version import parse as parse_version
  12. from pip._vendor.rich.console import Group
  13. from pip._vendor.rich.markup import escape
  14. from pip._vendor.rich.text import Text
  15. from pip._internal.index.collector import LinkCollector
  16. from pip._internal.index.package_finder import PackageFinder
  17. from pip._internal.metadata import get_default_environment
  18. from pip._internal.metadata.base import DistributionVersion
  19. from pip._internal.models.selection_prefs import SelectionPreferences
  20. from pip._internal.network.session import PipSession
  21. from pip._internal.utils.compat import WINDOWS
  22. from pip._internal.utils.entrypoints import (
  23. get_best_invocation_for_this_pip,
  24. get_best_invocation_for_this_python,
  25. )
  26. from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
  27. from pip._internal.utils.misc import ensure_dir
  28. _WEEK = datetime.timedelta(days=7)
  29. logger = logging.getLogger(__name__)
  30. def _get_statefile_name(key: str) -> str:
  31. key_bytes = key.encode()
  32. name = hashlib.sha224(key_bytes).hexdigest()
  33. return name
  34. def _convert_date(isodate: str) -> datetime.datetime:
  35. """Convert an ISO format string to a date.
  36. Handles the format 2020-01-22T14:24:01Z (trailing Z)
  37. which is not supported by older versions of fromisoformat.
  38. """
  39. return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
  40. class SelfCheckState:
  41. def __init__(self, cache_dir: str) -> None:
  42. self._state: Dict[str, Any] = {}
  43. self._statefile_path = None
  44. # Try to load the existing state
  45. if cache_dir:
  46. self._statefile_path = os.path.join(
  47. cache_dir, "selfcheck", _get_statefile_name(self.key)
  48. )
  49. try:
  50. with open(self._statefile_path, encoding="utf-8") as statefile:
  51. self._state = json.load(statefile)
  52. except (OSError, ValueError, KeyError):
  53. # Explicitly suppressing exceptions, since we don't want to
  54. # error out if the cache file is invalid.
  55. pass
  56. @property
  57. def key(self) -> str:
  58. return sys.prefix
  59. def get(self, current_time: datetime.datetime) -> Optional[str]:
  60. """Check if we have a not-outdated version loaded already."""
  61. if not self._state:
  62. return None
  63. if "last_check" not in self._state:
  64. return None
  65. if "pypi_version" not in self._state:
  66. return None
  67. # Determine if we need to refresh the state
  68. last_check = _convert_date(self._state["last_check"])
  69. time_since_last_check = current_time - last_check
  70. if time_since_last_check > _WEEK:
  71. return None
  72. return self._state["pypi_version"]
  73. def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
  74. # If we do not have a path to cache in, don't bother saving.
  75. if not self._statefile_path:
  76. return
  77. # Check to make sure that we own the directory
  78. if not check_path_owner(os.path.dirname(self._statefile_path)):
  79. return
  80. # Now that we've ensured the directory is owned by this user, we'll go
  81. # ahead and make sure that all our directories are created.
  82. ensure_dir(os.path.dirname(self._statefile_path))
  83. state = {
  84. # Include the key so it's easy to tell which pip wrote the
  85. # file.
  86. "key": self.key,
  87. "last_check": current_time.isoformat(),
  88. "pypi_version": pypi_version,
  89. }
  90. text = json.dumps(state, sort_keys=True, separators=(",", ":"))
  91. with adjacent_tmp_file(self._statefile_path) as f:
  92. f.write(text.encode())
  93. try:
  94. # Since we have a prefix-specific state file, we can just
  95. # overwrite whatever is there, no need to check.
  96. replace(f.name, self._statefile_path)
  97. except OSError:
  98. # Best effort.
  99. pass
  100. @dataclass
  101. class UpgradePrompt:
  102. old: str
  103. new: str
  104. def __rich__(self) -> Group:
  105. if WINDOWS:
  106. pip_cmd = f"{get_best_invocation_for_this_python()} -m pip"
  107. else:
  108. pip_cmd = get_best_invocation_for_this_pip()
  109. notice = "[bold][[reset][blue]notice[reset][bold]][reset]"
  110. return Group(
  111. Text(),
  112. Text.from_markup(
  113. f"{notice} A new release of pip is available: "
  114. f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
  115. ),
  116. Text.from_markup(
  117. f"{notice} To update, run: "
  118. f"[green]{escape(pip_cmd)} install --upgrade pip"
  119. ),
  120. )
  121. def was_installed_by_pip(pkg: str) -> bool:
  122. """Checks whether pkg was installed by pip
  123. This is used not to display the upgrade message when pip is in fact
  124. installed by system package manager, such as dnf on Fedora.
  125. """
  126. dist = get_default_environment().get_distribution(pkg)
  127. return dist is not None and "pip" == dist.installer
  128. def _get_current_remote_pip_version(
  129. session: PipSession, options: optparse.Values
  130. ) -> Optional[str]:
  131. # Lets use PackageFinder to see what the latest pip version is
  132. link_collector = LinkCollector.create(
  133. session,
  134. options=options,
  135. suppress_no_index=True,
  136. )
  137. # Pass allow_yanked=False so we don't suggest upgrading to a
  138. # yanked version.
  139. selection_prefs = SelectionPreferences(
  140. allow_yanked=False,
  141. allow_all_prereleases=False, # Explicitly set to False
  142. )
  143. finder = PackageFinder.create(
  144. link_collector=link_collector,
  145. selection_prefs=selection_prefs,
  146. )
  147. best_candidate = finder.find_best_candidate("pip").best_candidate
  148. if best_candidate is None:
  149. return None
  150. return str(best_candidate.version)
  151. def _self_version_check_logic(
  152. *,
  153. state: SelfCheckState,
  154. current_time: datetime.datetime,
  155. local_version: DistributionVersion,
  156. get_remote_version: Callable[[], Optional[str]],
  157. ) -> Optional[UpgradePrompt]:
  158. remote_version_str = state.get(current_time)
  159. if remote_version_str is None:
  160. remote_version_str = get_remote_version()
  161. if remote_version_str is None:
  162. logger.debug("No remote pip version found")
  163. return None
  164. state.set(remote_version_str, current_time)
  165. remote_version = parse_version(remote_version_str)
  166. logger.debug("Remote version of pip: %s", remote_version)
  167. logger.debug("Local version of pip: %s", local_version)
  168. pip_installed_by_pip = was_installed_by_pip("pip")
  169. logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
  170. if not pip_installed_by_pip:
  171. return None # Only suggest upgrade if pip is installed by pip.
  172. local_version_is_older = (
  173. local_version < remote_version
  174. and local_version.base_version != remote_version.base_version
  175. )
  176. if local_version_is_older:
  177. return UpgradePrompt(old=str(local_version), new=remote_version_str)
  178. return None
  179. def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
  180. """Check for an update for pip.
  181. Limit the frequency of checks to once per week. State is stored either in
  182. the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
  183. of the pip script path.
  184. """
  185. installed_dist = get_default_environment().get_distribution("pip")
  186. if not installed_dist:
  187. return
  188. try:
  189. upgrade_prompt = _self_version_check_logic(
  190. state=SelfCheckState(cache_dir=options.cache_dir),
  191. current_time=datetime.datetime.now(datetime.timezone.utc),
  192. local_version=installed_dist.version,
  193. get_remote_version=functools.partial(
  194. _get_current_remote_pip_version, session, options
  195. ),
  196. )
  197. if upgrade_prompt is not None:
  198. logger.warning("%s", upgrade_prompt, extra={"rich": True})
  199. except Exception:
  200. logger.warning("There was an error checking the latest version of pip.")
  201. logger.debug("See below for error", exc_info=True)