application.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. """
  2. A base Application class for Jupyter applications.
  3. All Jupyter applications should inherit from this.
  4. """
  5. # Copyright (c) Jupyter Development Team.
  6. # Distributed under the terms of the Modified BSD License.
  7. from __future__ import annotations
  8. import logging
  9. import os
  10. import sys
  11. import typing as t
  12. from copy import deepcopy
  13. from pathlib import Path
  14. from shutil import which
  15. from traitlets import Bool, List, Unicode, observe
  16. from traitlets.config.application import Application, catch_config_error
  17. from traitlets.config.loader import ConfigFileNotFound
  18. from .paths import (
  19. allow_insecure_writes,
  20. issue_insecure_write_warning,
  21. jupyter_config_dir,
  22. jupyter_config_path,
  23. jupyter_data_dir,
  24. jupyter_path,
  25. jupyter_runtime_dir,
  26. )
  27. from .utils import ensure_dir_exists, ensure_event_loop
  28. # mypy: disable-error-code="no-untyped-call"
  29. # aliases and flags
  30. base_aliases: dict[str, t.Any] = {}
  31. if isinstance(Application.aliases, dict):
  32. # traitlets 5
  33. base_aliases.update(Application.aliases)
  34. _jupyter_aliases = {
  35. "log-level": "Application.log_level",
  36. "config": "JupyterApp.config_file",
  37. }
  38. base_aliases.update(_jupyter_aliases)
  39. base_flags: dict[str, t.Any] = {}
  40. if isinstance(Application.flags, dict):
  41. # traitlets 5
  42. base_flags.update(Application.flags)
  43. _jupyter_flags: dict[str, t.Any] = {
  44. "debug": (
  45. {"Application": {"log_level": logging.DEBUG}},
  46. "set log level to logging.DEBUG (maximize logging output)",
  47. ),
  48. "generate-config": ({"JupyterApp": {"generate_config": True}}, "generate default config file"),
  49. "y": (
  50. {"JupyterApp": {"answer_yes": True}},
  51. "Answer yes to any questions instead of prompting.",
  52. ),
  53. }
  54. base_flags.update(_jupyter_flags)
  55. class NoStart(Exception):
  56. """Exception to raise when an application shouldn't start"""
  57. class JupyterApp(Application):
  58. """Base class for Jupyter applications"""
  59. name = "jupyter" # override in subclasses
  60. description = "A Jupyter Application"
  61. aliases = base_aliases
  62. flags = base_flags
  63. def _log_level_default(self) -> int:
  64. return logging.INFO
  65. jupyter_path = List(Unicode())
  66. def _jupyter_path_default(self) -> list[str]:
  67. return jupyter_path()
  68. config_dir = Unicode()
  69. def _config_dir_default(self) -> str:
  70. return jupyter_config_dir()
  71. @property
  72. def config_file_paths(self) -> list[str]:
  73. path = jupyter_config_path()
  74. if self.config_dir not in path:
  75. # Insert config dir as first item.
  76. path.insert(0, self.config_dir)
  77. return path
  78. data_dir = Unicode()
  79. def _data_dir_default(self) -> str:
  80. d = jupyter_data_dir()
  81. ensure_dir_exists(d, mode=0o700)
  82. return d
  83. runtime_dir = Unicode()
  84. def _runtime_dir_default(self) -> str:
  85. rd = jupyter_runtime_dir()
  86. ensure_dir_exists(rd, mode=0o700)
  87. return rd
  88. @observe("runtime_dir")
  89. def _runtime_dir_changed(self, change: t.Any) -> None:
  90. ensure_dir_exists(change["new"], mode=0o700)
  91. generate_config = Bool(False, config=True, help="""Generate default config file.""")
  92. config_file_name = Unicode(config=True, help="Specify a config file to load.")
  93. def _config_file_name_default(self) -> str:
  94. if not self.name:
  95. return ""
  96. return self.name.replace("-", "_") + "_config"
  97. config_file = Unicode(
  98. config=True,
  99. help="""Full path of a config file.""",
  100. )
  101. answer_yes = Bool(False, config=True, help="""Answer yes to any prompts.""")
  102. def write_default_config(self) -> None:
  103. """Write our default config to a .py config file"""
  104. config_file: str
  105. if self.config_file:
  106. config_file = self.config_file
  107. else:
  108. config_file = str(Path(self.config_dir, self.config_file_name + ".py"))
  109. if Path(config_file).exists() and not self.answer_yes:
  110. answer = ""
  111. def ask() -> str:
  112. prompt = f"Overwrite {config_file!r} with default config? [y/N]"
  113. try:
  114. return input(prompt).lower() or "n"
  115. except KeyboardInterrupt:
  116. print("") # empty line
  117. return "n"
  118. answer = ask()
  119. while not answer.startswith(("y", "n")):
  120. print("Please answer 'yes' or 'no'")
  121. answer = ask()
  122. if answer.startswith("n"):
  123. return
  124. config_text = self.generate_config_file()
  125. print(f"Writing default config to: {config_file!r}")
  126. ensure_dir_exists(Path(config_file).parent.resolve(), 0o700)
  127. with Path.open(Path(config_file), mode="w", encoding="utf-8") as f:
  128. f.write(config_text)
  129. def migrate_config(self) -> None:
  130. """Migrate config/data from IPython 3"""
  131. try: # let's see if we can open the marker file
  132. # for reading and updating (writing)
  133. f_marker = Path.open(Path(self.config_dir, "migrated"), "r+")
  134. except FileNotFoundError: # cannot find the marker file
  135. pass # that means we have not migrated yet, so continue
  136. except OSError: # not readable and/or writable
  137. return # so let's give up migration in such an environment
  138. else: # if we got here without raising anything,
  139. # that means the file exists
  140. f_marker.close()
  141. return # so we must have already migrated -> bail out
  142. from .migrate import get_ipython_dir, migrate # noqa: PLC0415
  143. # No IPython dir, nothing to migrate
  144. if not Path(get_ipython_dir()).exists():
  145. return
  146. migrate()
  147. def load_config_file(self, suppress_errors: bool = True) -> None: # type:ignore[override]
  148. """Load the config file.
  149. By default, errors in loading config are handled, and a warning
  150. printed on screen. For testing, the suppress_errors option is set
  151. to False, so errors will make tests fail.
  152. """
  153. self.log.debug("Searching %s for config files", self.config_file_paths)
  154. base_config = "jupyter_config"
  155. try:
  156. super().load_config_file(
  157. base_config,
  158. path=self.config_file_paths,
  159. )
  160. except ConfigFileNotFound:
  161. # ignore errors loading parent
  162. self.log.debug("Config file %s not found", base_config)
  163. if self.config_file:
  164. path, config_file_name = os.path.split(self.config_file)
  165. else:
  166. path = self.config_file_paths # type:ignore[assignment]
  167. config_file_name = self.config_file_name
  168. if not config_file_name or (config_file_name == base_config):
  169. return
  170. try:
  171. super().load_config_file(config_file_name, path=path)
  172. except ConfigFileNotFound:
  173. self.log.debug("Config file not found, skipping: %s", config_file_name)
  174. except Exception:
  175. # Reraise errors for testing purposes, or if set in
  176. # self.raise_config_file_errors
  177. if (not suppress_errors) or self.raise_config_file_errors:
  178. raise
  179. self.log.warning("Error loading config file: %s", config_file_name, exc_info=True)
  180. # subcommand-related
  181. def _find_subcommand(self, name: str) -> str:
  182. name = f"{self.name}-{name}"
  183. return which(name) or ""
  184. @property
  185. def _dispatching(self) -> bool:
  186. """Return whether we are dispatching to another command
  187. or running ourselves.
  188. """
  189. return bool(self.generate_config or self.subapp or self.subcommand)
  190. subcommand = Unicode()
  191. @catch_config_error
  192. def initialize(self, argv: t.Any = None) -> None:
  193. """Initialize the application."""
  194. # don't hook up crash handler before parsing command-line
  195. if argv is None:
  196. argv = sys.argv[1:]
  197. if argv:
  198. subc = self._find_subcommand(argv[0])
  199. if subc:
  200. self.argv = argv
  201. self.subcommand = subc
  202. return
  203. self.parse_command_line(argv)
  204. cl_config = deepcopy(self.config)
  205. if self._dispatching:
  206. return
  207. self.migrate_config()
  208. self.load_config_file()
  209. # enforce cl-opts override configfile opts:
  210. self.update_config(cl_config)
  211. if allow_insecure_writes:
  212. issue_insecure_write_warning()
  213. def start(self) -> None:
  214. """Start the whole thing"""
  215. if self.subcommand:
  216. os.execv(self.subcommand, [self.subcommand, *self.argv[1:]]) # noqa: S606
  217. raise NoStart()
  218. if self.subapp:
  219. self.subapp.start()
  220. raise NoStart()
  221. if self.generate_config:
  222. self.write_default_config()
  223. raise NoStart()
  224. @classmethod
  225. def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
  226. """Launch an instance of a Jupyter Application"""
  227. # Ensure an event loop is set before any other code runs.
  228. loop = ensure_event_loop()
  229. try:
  230. super().launch_instance(argv=argv, **kwargs)
  231. except NoStart:
  232. return
  233. loop.close()
  234. class JupyterAsyncApp(JupyterApp):
  235. """A Jupyter application that runs on an asyncio loop."""
  236. name = "jupyter_async" # override in subclasses
  237. description = "An Async Jupyter Application"
  238. # Set to True for tornado-based apps.
  239. _prefer_selector_loop = False
  240. async def initialize_async(self, argv: t.Any = None) -> None:
  241. """Initialize the application asynchronoously."""
  242. async def start_async(self) -> None:
  243. """Run the application in an event loop."""
  244. @classmethod
  245. async def _launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
  246. app = cls.instance(**kwargs)
  247. app.initialize(argv)
  248. await app.initialize_async(argv)
  249. await app.start_async()
  250. @classmethod
  251. def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
  252. """Launch an instance of an async Jupyter Application"""
  253. loop = ensure_event_loop(cls._prefer_selector_loop)
  254. coro = cls._launch_instance(argv, **kwargs)
  255. loop.run_until_complete(coro)
  256. loop.close()
  257. if __name__ == "__main__":
  258. JupyterApp.launch_instance()