serverextension.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. """Utilities for installing extensions"""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import annotations
  5. import logging
  6. import os
  7. import sys
  8. import typing as t
  9. from jupyter_core.application import JupyterApp
  10. from jupyter_core.paths import ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH, jupyter_config_dir
  11. from tornado.log import LogFormatter
  12. from traitlets import Bool
  13. from jupyter_server._version import __version__
  14. from jupyter_server.extension.config import ExtensionConfigManager
  15. from jupyter_server.extension.manager import ExtensionManager, ExtensionPackage
  16. def _get_config_dir(user: bool = False, sys_prefix: bool = False) -> str:
  17. """Get the location of config files for the current context
  18. Returns the string to the environment
  19. Parameters
  20. ----------
  21. user : bool [default: False]
  22. Get the user's .jupyter config directory
  23. sys_prefix : bool [default: False]
  24. Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
  25. """
  26. if user and sys_prefix:
  27. sys_prefix = False
  28. if user:
  29. extdir = jupyter_config_dir()
  30. elif sys_prefix:
  31. extdir = ENV_CONFIG_PATH[0]
  32. else:
  33. extdir = SYSTEM_CONFIG_PATH[0]
  34. return extdir
  35. def _get_extmanager_for_context(
  36. write_dir: str = "jupyter_server_config.d", user: bool = False, sys_prefix: bool = False
  37. ) -> tuple[str, ExtensionManager]:
  38. """Get an extension manager pointing at the current context
  39. Returns the path to the current context and an ExtensionManager object.
  40. Parameters
  41. ----------
  42. write_dir : str [default: 'jupyter_server_config.d']
  43. Name of config directory to write extension config.
  44. user : bool [default: False]
  45. Get the user's .jupyter config directory
  46. sys_prefix : bool [default: False]
  47. Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
  48. """
  49. config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
  50. config_manager = ExtensionConfigManager(
  51. read_config_path=[config_dir],
  52. write_config_dir=os.path.join(config_dir, write_dir),
  53. )
  54. extension_manager = ExtensionManager(
  55. config_manager=config_manager,
  56. )
  57. return config_dir, extension_manager
  58. class ArgumentConflict(ValueError):
  59. pass
  60. _base_flags: dict[str, t.Any] = {}
  61. _base_flags.update(JupyterApp.flags)
  62. _base_flags.pop("y", None)
  63. _base_flags.pop("generate-config", None)
  64. _base_flags.update(
  65. {
  66. "user": (
  67. {
  68. "BaseExtensionApp": {
  69. "user": True,
  70. }
  71. },
  72. "Apply the operation only for the given user",
  73. ),
  74. "system": (
  75. {
  76. "BaseExtensionApp": {
  77. "user": False,
  78. "sys_prefix": False,
  79. }
  80. },
  81. "Apply the operation system-wide",
  82. ),
  83. "sys-prefix": (
  84. {
  85. "BaseExtensionApp": {
  86. "sys_prefix": True,
  87. }
  88. },
  89. "Use sys.prefix as the prefix for installing extensions (for environments, packaging)",
  90. ),
  91. "py": (
  92. {
  93. "BaseExtensionApp": {
  94. "python": True,
  95. }
  96. },
  97. "Install from a Python package",
  98. ),
  99. }
  100. )
  101. _base_flags["python"] = _base_flags["py"]
  102. _base_aliases: dict[str, t.Any] = {}
  103. _base_aliases.update(JupyterApp.aliases)
  104. class BaseExtensionApp(JupyterApp):
  105. """Base extension installer app"""
  106. _log_formatter_cls = LogFormatter # type:ignore[assignment]
  107. flags = _base_flags
  108. aliases = _base_aliases
  109. version = __version__
  110. user = Bool(False, config=True, help="Whether to do a user install")
  111. sys_prefix = Bool(True, config=True, help="Use the sys.prefix as the prefix")
  112. python = Bool(False, config=True, help="Install from a Python package")
  113. def _log_format_default(self) -> str:
  114. """A default format for messages"""
  115. return "%(message)s"
  116. @property
  117. def config_dir(self) -> str: # type:ignore[override]
  118. return _get_config_dir(user=self.user, sys_prefix=self.sys_prefix)
  119. # Constants for pretty print extension listing function.
  120. # Window doesn't support coloring in the commandline
  121. GREEN_ENABLED = "\033[32menabled\033[0m" if os.name != "nt" else "enabled"
  122. RED_DISABLED = "\033[31mdisabled\033[0m" if os.name != "nt" else "disabled"
  123. GREEN_OK = "\033[32mOK\033[0m" if os.name != "nt" else "ok"
  124. RED_X = "\033[31m X\033[0m" if os.name != "nt" else " X"
  125. # ------------------------------------------------------------------------------
  126. # Public API
  127. # ------------------------------------------------------------------------------
  128. def toggle_server_extension_python(
  129. import_name: str,
  130. enabled: bool | None = None,
  131. parent: t.Any = None,
  132. user: bool = False,
  133. sys_prefix: bool = True,
  134. ) -> None:
  135. """Toggle the boolean setting for a given server extension
  136. in a Jupyter config file.
  137. """
  138. sys_prefix = False if user else sys_prefix
  139. config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
  140. manager = ExtensionConfigManager(
  141. read_config_path=[config_dir],
  142. write_config_dir=os.path.join(config_dir, "jupyter_server_config.d"),
  143. )
  144. if enabled:
  145. manager.enable(import_name)
  146. else:
  147. manager.disable(import_name)
  148. # ----------------------------------------------------------------------
  149. # Applications
  150. # ----------------------------------------------------------------------
  151. flags = {}
  152. flags.update(BaseExtensionApp.flags)
  153. flags.pop("y", None)
  154. flags.pop("generate-config", None)
  155. flags.update(
  156. {
  157. "user": (
  158. {
  159. "ToggleServerExtensionApp": {
  160. "user": True,
  161. }
  162. },
  163. "Perform the operation for the current user",
  164. ),
  165. "system": (
  166. {
  167. "ToggleServerExtensionApp": {
  168. "user": False,
  169. "sys_prefix": False,
  170. }
  171. },
  172. "Perform the operation system-wide",
  173. ),
  174. "sys-prefix": (
  175. {
  176. "ToggleServerExtensionApp": {
  177. "sys_prefix": True,
  178. }
  179. },
  180. "Use sys.prefix as the prefix for installing server extensions",
  181. ),
  182. "py": (
  183. {
  184. "ToggleServerExtensionApp": {
  185. "python": True,
  186. }
  187. },
  188. "Install from a Python package",
  189. ),
  190. }
  191. )
  192. flags["python"] = flags["py"]
  193. _desc = "Enable/disable a server extension using frontend configuration files."
  194. class ToggleServerExtensionApp(BaseExtensionApp):
  195. """A base class for enabling/disabling extensions"""
  196. name = "jupyter server extension enable/disable"
  197. description = _desc
  198. flags = flags
  199. _toggle_value = Bool()
  200. _toggle_pre_message = ""
  201. _toggle_post_message = ""
  202. def toggle_server_extension(self, import_name: str) -> None:
  203. """Change the status of a named server extension.
  204. Uses the value of `self._toggle_value`.
  205. Parameters
  206. ---------
  207. import_name : str
  208. Importable Python module (dotted-notation) exposing the magic-named
  209. `load_jupyter_server_extension` function
  210. """
  211. # Create an extension manager for this instance.
  212. config_dir, extension_manager = _get_extmanager_for_context(
  213. user=self.user, sys_prefix=self.sys_prefix
  214. )
  215. try:
  216. self.log.info(f"{self._toggle_pre_message.capitalize()}: {import_name}")
  217. self.log.info(f"- Writing config: {config_dir}")
  218. # Validate the server extension.
  219. self.log.info(f" - Validating {import_name}...")
  220. config = extension_manager.config_manager
  221. enabled = False
  222. if config:
  223. jpserver_extensions = config.get_jpserver_extensions()
  224. if import_name not in jpserver_extensions:
  225. msg = (
  226. f"The module '{import_name}' could not be found. Are you "
  227. "sure the extension is installed?"
  228. )
  229. raise ValueError(msg)
  230. enabled = jpserver_extensions[import_name]
  231. # Interface with the Extension Package and validate.
  232. extpkg = ExtensionPackage(name=import_name, enabled=enabled)
  233. if not extpkg.validate():
  234. msg = "validation failed"
  235. raise ValueError(msg)
  236. version = extpkg.version
  237. self.log.info(f" {import_name} {version} {GREEN_OK}")
  238. # Toggle extension config.
  239. config = extension_manager.config_manager
  240. if config:
  241. if self._toggle_value is True:
  242. config.enable(import_name)
  243. else:
  244. config.disable(import_name)
  245. # If successful, let's log.
  246. self.log.info(f" - Extension successfully {self._toggle_post_message}.")
  247. except Exception as err:
  248. self.log.error(f" {RED_X} Validation failed: {err}")
  249. def start(self) -> None:
  250. """Perform the App's actions as configured"""
  251. if not self.extra_args:
  252. sys.exit("Please specify a server extension/package to enable or disable")
  253. for arg in self.extra_args:
  254. self.toggle_server_extension(arg)
  255. class EnableServerExtensionApp(ToggleServerExtensionApp):
  256. """An App that enables (and validates) Server Extensions"""
  257. name = "jupyter server extension enable"
  258. description = """
  259. Enable a server extension in configuration.
  260. Usage
  261. jupyter server extension enable [--system|--sys-prefix]
  262. """
  263. _toggle_value = True # type:ignore[assignment]
  264. _toggle_pre_message = "enabling"
  265. _toggle_post_message = "enabled"
  266. class DisableServerExtensionApp(ToggleServerExtensionApp):
  267. """An App that disables Server Extensions"""
  268. name = "jupyter server extension disable"
  269. description = """
  270. Disable a server extension in configuration.
  271. Usage
  272. jupyter server extension disable [--system|--sys-prefix]
  273. """
  274. _toggle_value = False # type:ignore[assignment]
  275. _toggle_pre_message = "disabling"
  276. _toggle_post_message = "disabled"
  277. class ListServerExtensionsApp(BaseExtensionApp):
  278. """An App that lists (and validates) Server Extensions"""
  279. name = "jupyter server extension list"
  280. version = __version__
  281. description = "List all server extensions known by the configuration system"
  282. def list_server_extensions(self) -> None:
  283. """List all enabled and disabled server extensions, by config path
  284. Enabled extensions are validated, potentially generating warnings.
  285. """
  286. configurations = (
  287. {"user": True, "sys_prefix": False},
  288. {"user": False, "sys_prefix": True},
  289. {"user": False, "sys_prefix": False},
  290. )
  291. for option in configurations:
  292. config_dir = _get_config_dir(**option)
  293. print(f"Config dir: {config_dir}")
  294. write_dir = "jupyter_server_config.d"
  295. config_manager = ExtensionConfigManager(
  296. read_config_path=[config_dir],
  297. write_config_dir=os.path.join(config_dir, write_dir),
  298. )
  299. jpserver_extensions = config_manager.get_jpserver_extensions()
  300. for name, enabled in jpserver_extensions.items():
  301. # Attempt to get extension metadata
  302. print(f" {name} {GREEN_ENABLED if enabled else RED_DISABLED}")
  303. try:
  304. print(f" - Validating {name}...")
  305. extension = ExtensionPackage(name=name, enabled=enabled)
  306. if not extension.validate():
  307. msg = "validation failed"
  308. raise ValueError(msg)
  309. version = extension.version
  310. print(f" {name} {version} {GREEN_OK}")
  311. except Exception as err:
  312. self.log.debug("", exc_info=True)
  313. print(f" {RED_X} {err}")
  314. # Add a blank line between paths.
  315. self.log.info("")
  316. def start(self) -> None:
  317. """Perform the App's actions as configured"""
  318. self.list_server_extensions()
  319. _examples = """
  320. jupyter server extension list # list all configured server extensions
  321. jupyter server extension enable --py <packagename> # enable all server extensions in a Python package
  322. jupyter server extension disable --py <packagename> # disable all server extensions in a Python package
  323. """
  324. class ServerExtensionApp(BaseExtensionApp):
  325. """Root level server extension app"""
  326. name = "jupyter server extension"
  327. version = __version__
  328. description: str = "Work with Jupyter server extensions"
  329. examples = _examples
  330. subcommands: dict[str, t.Any] = {
  331. "enable": (EnableServerExtensionApp, "Enable a server extension"),
  332. "disable": (DisableServerExtensionApp, "Disable a server extension"),
  333. "list": (ListServerExtensionsApp, "List server extensions"),
  334. }
  335. def start(self) -> None:
  336. """Perform the App's actions as configured"""
  337. super().start()
  338. # The above should have called a subcommand and raised NoStart; if we
  339. # get here, it didn't, so we should self.log.info a message.
  340. subcmds = ", ".join(sorted(self.subcommands))
  341. sys.exit("Please supply at least one subcommand: %s" % subcmds)
  342. main = ServerExtensionApp.launch_instance
  343. if __name__ == "__main__":
  344. main()