console.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. """Functions to communicate to the user via console."""
  2. from __future__ import annotations
  3. import inspect
  4. import shutil
  5. from pathlib import Path
  6. from types import FrameType
  7. from rich.console import Console
  8. from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
  9. from rich.prompt import Prompt
  10. from reflex.constants import LogLevel
  11. # Console for pretty printing.
  12. _console = Console()
  13. # The current log level.
  14. _LOG_LEVEL = LogLevel.INFO
  15. # Deprecated features who's warning has been printed.
  16. _EMITTED_DEPRECATION_WARNINGS = set()
  17. # Info messages which have been printed.
  18. _EMITTED_INFO = set()
  19. # Warnings which have been printed.
  20. _EMIITED_WARNINGS = set()
  21. # Errors which have been printed.
  22. _EMITTED_ERRORS = set()
  23. # Success messages which have been printed.
  24. _EMITTED_SUCCESS = set()
  25. # Debug messages which have been printed.
  26. _EMITTED_DEBUG = set()
  27. # Logs which have been printed.
  28. _EMITTED_LOGS = set()
  29. # Prints which have been printed.
  30. _EMITTED_PRINTS = set()
  31. def set_log_level(log_level: LogLevel):
  32. """Set the log level.
  33. Args:
  34. log_level: The log level to set.
  35. Raises:
  36. ValueError: If the log level is invalid.
  37. """
  38. if not isinstance(log_level, LogLevel):
  39. deprecate(
  40. feature_name="Passing a string to set_log_level",
  41. reason="use reflex.constants.LogLevel enum instead",
  42. deprecation_version="0.6.6",
  43. removal_version="0.7.0",
  44. )
  45. try:
  46. log_level = getattr(LogLevel, log_level.upper())
  47. except AttributeError as ae:
  48. raise ValueError(f"Invalid log level: {log_level}") from ae
  49. global _LOG_LEVEL
  50. _LOG_LEVEL = log_level
  51. def is_debug() -> bool:
  52. """Check if the log level is debug.
  53. Returns:
  54. True if the log level is debug.
  55. """
  56. return _LOG_LEVEL <= LogLevel.DEBUG
  57. def print(msg: str, dedupe: bool = False, **kwargs):
  58. """Print a message.
  59. Args:
  60. msg: The message to print.
  61. dedupe: If True, suppress multiple console logs of print message.
  62. kwargs: Keyword arguments to pass to the print function.
  63. """
  64. if dedupe:
  65. if msg in _EMITTED_PRINTS:
  66. return
  67. else:
  68. _EMITTED_PRINTS.add(msg)
  69. _console.print(msg, **kwargs)
  70. def debug(msg: str, dedupe: bool = False, **kwargs):
  71. """Print a debug message.
  72. Args:
  73. msg: The debug message.
  74. dedupe: If True, suppress multiple console logs of debug message.
  75. kwargs: Keyword arguments to pass to the print function.
  76. """
  77. if is_debug():
  78. msg_ = f"[purple]Debug: {msg}[/purple]"
  79. if dedupe:
  80. if msg_ in _EMITTED_DEBUG:
  81. return
  82. else:
  83. _EMITTED_DEBUG.add(msg_)
  84. if progress := kwargs.pop("progress", None):
  85. progress.console.print(msg_, **kwargs)
  86. else:
  87. print(msg_, **kwargs)
  88. def info(msg: str, dedupe: bool = False, **kwargs):
  89. """Print an info message.
  90. Args:
  91. msg: The info message.
  92. dedupe: If True, suppress multiple console logs of info message.
  93. kwargs: Keyword arguments to pass to the print function.
  94. """
  95. if _LOG_LEVEL <= LogLevel.INFO:
  96. if dedupe:
  97. if msg in _EMITTED_INFO:
  98. return
  99. else:
  100. _EMITTED_INFO.add(msg)
  101. print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
  102. def success(msg: str, dedupe: bool = False, **kwargs):
  103. """Print a success message.
  104. Args:
  105. msg: The success message.
  106. dedupe: If True, suppress multiple console logs of success message.
  107. kwargs: Keyword arguments to pass to the print function.
  108. """
  109. if _LOG_LEVEL <= LogLevel.INFO:
  110. if dedupe:
  111. if msg in _EMITTED_SUCCESS:
  112. return
  113. else:
  114. _EMITTED_SUCCESS.add(msg)
  115. print(f"[green]Success: {msg}[/green]", **kwargs)
  116. def log(msg: str, dedupe: bool = False, **kwargs):
  117. """Takes a string and logs it to the console.
  118. Args:
  119. msg: The message to log.
  120. dedupe: If True, suppress multiple console logs of log message.
  121. kwargs: Keyword arguments to pass to the print function.
  122. """
  123. if _LOG_LEVEL <= LogLevel.INFO:
  124. if dedupe:
  125. if msg in _EMITTED_LOGS:
  126. return
  127. else:
  128. _EMITTED_LOGS.add(msg)
  129. _console.log(msg, **kwargs)
  130. def rule(title: str, **kwargs):
  131. """Prints a horizontal rule with a title.
  132. Args:
  133. title: The title of the rule.
  134. kwargs: Keyword arguments to pass to the print function.
  135. """
  136. _console.rule(title, **kwargs)
  137. def warn(msg: str, dedupe: bool = False, **kwargs):
  138. """Print a warning message.
  139. Args:
  140. msg: The warning message.
  141. dedupe: If True, suppress multiple console logs of warning message.
  142. kwargs: Keyword arguments to pass to the print function.
  143. """
  144. if _LOG_LEVEL <= LogLevel.WARNING:
  145. if dedupe:
  146. if msg in _EMIITED_WARNINGS:
  147. return
  148. else:
  149. _EMIITED_WARNINGS.add(msg)
  150. print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
  151. def _get_first_non_framework_frame() -> FrameType | None:
  152. import click
  153. import typer
  154. import typing_extensions
  155. import reflex as rx
  156. # Exclude utility modules that should never be the source of deprecated reflex usage.
  157. exclude_modules = [click, rx, typer, typing_extensions]
  158. exclude_roots = [
  159. p.parent.resolve()
  160. if (p := Path(m.__file__)).name == "__init__.py"
  161. else p.resolve()
  162. for m in exclude_modules
  163. ]
  164. # Specifically exclude the reflex cli module.
  165. if reflex_bin := shutil.which(b"reflex"):
  166. exclude_roots.append(Path(reflex_bin.decode()))
  167. frame = inspect.currentframe()
  168. while frame := frame and frame.f_back:
  169. frame_path = Path(inspect.getfile(frame)).resolve()
  170. if not any(frame_path.is_relative_to(root) for root in exclude_roots):
  171. break
  172. return frame
  173. def deprecate(
  174. feature_name: str,
  175. reason: str,
  176. deprecation_version: str,
  177. removal_version: str,
  178. dedupe: bool = True,
  179. **kwargs,
  180. ):
  181. """Print a deprecation warning.
  182. Args:
  183. feature_name: The feature to deprecate.
  184. reason: The reason for deprecation.
  185. deprecation_version: The version the feature was deprecated
  186. removal_version: The version the deprecated feature will be removed
  187. dedupe: If True, suppress multiple console logs of deprecation message.
  188. kwargs: Keyword arguments to pass to the print function.
  189. """
  190. dedupe_key = feature_name
  191. loc = ""
  192. # See if we can find where the deprecation exists in "user code"
  193. origin_frame = _get_first_non_framework_frame()
  194. if origin_frame is not None:
  195. filename = Path(origin_frame.f_code.co_filename)
  196. if filename.is_relative_to(Path.cwd()):
  197. filename = filename.relative_to(Path.cwd())
  198. loc = f"{filename}:{origin_frame.f_lineno}"
  199. dedupe_key = f"{dedupe_key} {loc}"
  200. if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS:
  201. msg = (
  202. f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
  203. f"removed in {removal_version}. ({loc})"
  204. )
  205. if _LOG_LEVEL <= LogLevel.WARNING:
  206. print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
  207. if dedupe:
  208. _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key)
  209. def error(msg: str, dedupe: bool = False, **kwargs):
  210. """Print an error message.
  211. Args:
  212. msg: The error message.
  213. dedupe: If True, suppress multiple console logs of error message.
  214. kwargs: Keyword arguments to pass to the print function.
  215. """
  216. if _LOG_LEVEL <= LogLevel.ERROR:
  217. if dedupe:
  218. if msg in _EMITTED_ERRORS:
  219. return
  220. else:
  221. _EMITTED_ERRORS.add(msg)
  222. print(f"[red]{msg}[/red]", **kwargs)
  223. def ask(
  224. question: str,
  225. choices: list[str] | None = None,
  226. default: str | None = None,
  227. show_choices: bool = True,
  228. ) -> str:
  229. """Takes a prompt question and optionally a list of choices
  230. and returns the user input.
  231. Args:
  232. question: The question to ask the user.
  233. choices: A list of choices to select from.
  234. default: The default option selected.
  235. show_choices: Whether to show the choices.
  236. Returns:
  237. A string with the user input.
  238. """
  239. return Prompt.ask(
  240. question, choices=choices, default=default, show_choices=show_choices
  241. ) # type: ignore
  242. def progress():
  243. """Create a new progress bar.
  244. Returns:
  245. A new progress bar.
  246. """
  247. return Progress(
  248. *Progress.get_default_columns()[:-1],
  249. MofNCompleteColumn(),
  250. TimeElapsedColumn(),
  251. )
  252. def status(*args, **kwargs):
  253. """Create a status with a spinner.
  254. Args:
  255. *args: Args to pass to the status.
  256. **kwargs: Kwargs to pass to the status.
  257. Returns:
  258. A new status.
  259. """
  260. return _console.status(*args, **kwargs)