utils.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. from __future__ import annotations
  2. import os
  3. import signal
  4. import sys
  5. import threading
  6. from collections import deque
  7. from typing import (
  8. Callable,
  9. ContextManager,
  10. Deque,
  11. Dict,
  12. Generator,
  13. Generic,
  14. TypeVar,
  15. Union,
  16. )
  17. from wcwidth import wcwidth
  18. __all__ = [
  19. "Event",
  20. "DummyContext",
  21. "get_cwidth",
  22. "suspend_to_background_supported",
  23. "is_conemu_ansi",
  24. "is_windows",
  25. "in_main_thread",
  26. "get_bell_environment_variable",
  27. "get_term_environment_variable",
  28. "take_using_weights",
  29. "to_str",
  30. "to_int",
  31. "AnyFloat",
  32. "to_float",
  33. "is_dumb_terminal",
  34. ]
  35. # Used to ensure sphinx autodoc does not try to import platform-specific
  36. # stuff when documenting win32.py modules.
  37. SPHINX_AUTODOC_RUNNING = "sphinx.ext.autodoc" in sys.modules
  38. _Sender = TypeVar("_Sender", covariant=True)
  39. class Event(Generic[_Sender]):
  40. """
  41. Simple event to which event handlers can be attached. For instance::
  42. class Cls:
  43. def __init__(self):
  44. # Define event. The first parameter is the sender.
  45. self.event = Event(self)
  46. obj = Cls()
  47. def handler(sender):
  48. pass
  49. # Add event handler by using the += operator.
  50. obj.event += handler
  51. # Fire event.
  52. obj.event()
  53. """
  54. def __init__(
  55. self, sender: _Sender, handler: Callable[[_Sender], None] | None = None
  56. ) -> None:
  57. self.sender = sender
  58. self._handlers: list[Callable[[_Sender], None]] = []
  59. if handler is not None:
  60. self += handler
  61. def __call__(self) -> None:
  62. "Fire event."
  63. for handler in self._handlers:
  64. handler(self.sender)
  65. def fire(self) -> None:
  66. "Alias for just calling the event."
  67. self()
  68. def add_handler(self, handler: Callable[[_Sender], None]) -> None:
  69. """
  70. Add another handler to this callback.
  71. (Handler should be a callable that takes exactly one parameter: the
  72. sender object.)
  73. """
  74. # Add to list of event handlers.
  75. self._handlers.append(handler)
  76. def remove_handler(self, handler: Callable[[_Sender], None]) -> None:
  77. """
  78. Remove a handler from this callback.
  79. """
  80. if handler in self._handlers:
  81. self._handlers.remove(handler)
  82. def __iadd__(self, handler: Callable[[_Sender], None]) -> Event[_Sender]:
  83. """
  84. `event += handler` notation for adding a handler.
  85. """
  86. self.add_handler(handler)
  87. return self
  88. def __isub__(self, handler: Callable[[_Sender], None]) -> Event[_Sender]:
  89. """
  90. `event -= handler` notation for removing a handler.
  91. """
  92. self.remove_handler(handler)
  93. return self
  94. class DummyContext(ContextManager[None]):
  95. """
  96. (contextlib.nested is not available on Py3)
  97. """
  98. def __enter__(self) -> None:
  99. pass
  100. def __exit__(self, *a: object) -> None:
  101. pass
  102. class _CharSizesCache(Dict[str, int]):
  103. """
  104. Cache for wcwidth sizes.
  105. """
  106. LONG_STRING_MIN_LEN = 64 # Minimum string length for considering it long.
  107. MAX_LONG_STRINGS = 16 # Maximum number of long strings to remember.
  108. def __init__(self) -> None:
  109. super().__init__()
  110. # Keep track of the "long" strings in this cache.
  111. self._long_strings: Deque[str] = deque()
  112. def __missing__(self, string: str) -> int:
  113. # Note: We use the `max(0, ...` because some non printable control
  114. # characters, like e.g. Ctrl-underscore get a -1 wcwidth value.
  115. # It can be possible that these characters end up in the input
  116. # text.
  117. result: int
  118. if len(string) == 1:
  119. result = max(0, wcwidth(string))
  120. else:
  121. result = sum(self[c] for c in string)
  122. # Store in cache.
  123. self[string] = result
  124. # Rotate long strings.
  125. # (It's hard to tell what we can consider short...)
  126. if len(string) > self.LONG_STRING_MIN_LEN:
  127. long_strings = self._long_strings
  128. long_strings.append(string)
  129. if len(long_strings) > self.MAX_LONG_STRINGS:
  130. key_to_remove = long_strings.popleft()
  131. if key_to_remove in self:
  132. del self[key_to_remove]
  133. return result
  134. _CHAR_SIZES_CACHE = _CharSizesCache()
  135. def get_cwidth(string: str) -> int:
  136. """
  137. Return width of a string. Wrapper around ``wcwidth``.
  138. """
  139. return _CHAR_SIZES_CACHE[string]
  140. def suspend_to_background_supported() -> bool:
  141. """
  142. Returns `True` when the Python implementation supports
  143. suspend-to-background. This is typically `False' on Windows systems.
  144. """
  145. return hasattr(signal, "SIGTSTP")
  146. def is_windows() -> bool:
  147. """
  148. True when we are using Windows.
  149. """
  150. return sys.platform == "win32" # Not 'darwin' or 'linux2'
  151. def is_windows_vt100_supported() -> bool:
  152. """
  153. True when we are using Windows, but VT100 escape sequences are supported.
  154. """
  155. if sys.platform == "win32":
  156. # Import needs to be inline. Windows libraries are not always available.
  157. from prompt_toolkit.output.windows10 import is_win_vt100_enabled
  158. return is_win_vt100_enabled()
  159. return False
  160. def is_conemu_ansi() -> bool:
  161. """
  162. True when the ConEmu Windows console is used.
  163. """
  164. return sys.platform == "win32" and os.environ.get("ConEmuANSI", "OFF") == "ON"
  165. def in_main_thread() -> bool:
  166. """
  167. True when the current thread is the main thread.
  168. """
  169. return threading.current_thread().__class__.__name__ == "_MainThread"
  170. def get_bell_environment_variable() -> bool:
  171. """
  172. True if env variable is set to true (true, TRUE, True, 1).
  173. """
  174. value = os.environ.get("PROMPT_TOOLKIT_BELL", "true")
  175. return value.lower() in ("1", "true")
  176. def get_term_environment_variable() -> str:
  177. "Return the $TERM environment variable."
  178. return os.environ.get("TERM", "")
  179. _T = TypeVar("_T")
  180. def take_using_weights(
  181. items: list[_T], weights: list[int]
  182. ) -> Generator[_T, None, None]:
  183. """
  184. Generator that keeps yielding items from the items list, in proportion to
  185. their weight. For instance::
  186. # Getting the first 70 items from this generator should have yielded 10
  187. # times A, 20 times B and 40 times C, all distributed equally..
  188. take_using_weights(['A', 'B', 'C'], [5, 10, 20])
  189. :param items: List of items to take from.
  190. :param weights: Integers representing the weight. (Numbers have to be
  191. integers, not floats.)
  192. """
  193. assert len(items) == len(weights)
  194. assert len(items) > 0
  195. # Remove items with zero-weight.
  196. items2 = []
  197. weights2 = []
  198. for item, w in zip(items, weights):
  199. if w > 0:
  200. items2.append(item)
  201. weights2.append(w)
  202. items = items2
  203. weights = weights2
  204. # Make sure that we have some items left.
  205. if not items:
  206. raise ValueError("Did't got any items with a positive weight.")
  207. #
  208. already_taken = [0 for i in items]
  209. item_count = len(items)
  210. max_weight = max(weights)
  211. i = 0
  212. while True:
  213. # Each iteration of this loop, we fill up until by (total_weight/max_weight).
  214. adding = True
  215. while adding:
  216. adding = False
  217. for item_i, item, weight in zip(range(item_count), items, weights):
  218. if already_taken[item_i] < i * weight / float(max_weight):
  219. yield item
  220. already_taken[item_i] += 1
  221. adding = True
  222. i += 1
  223. def to_str(value: Callable[[], str] | str) -> str:
  224. "Turn callable or string into string."
  225. if callable(value):
  226. return to_str(value())
  227. else:
  228. return str(value)
  229. def to_int(value: Callable[[], int] | int) -> int:
  230. "Turn callable or int into int."
  231. if callable(value):
  232. return to_int(value())
  233. else:
  234. return int(value)
  235. AnyFloat = Union[Callable[[], float], float]
  236. def to_float(value: AnyFloat) -> float:
  237. "Turn callable or float into float."
  238. if callable(value):
  239. return to_float(value())
  240. else:
  241. return float(value)
  242. def is_dumb_terminal(term: str | None = None) -> bool:
  243. """
  244. True if this terminal type is considered "dumb".
  245. If so, we should fall back to the simplest possible form of line editing,
  246. without cursor positioning and color support.
  247. """
  248. if term is None:
  249. return is_dumb_terminal(os.environ.get("TERM", ""))
  250. return term.lower() in ["dumb", "unknown"]