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.

334 lines
11 KiB

4 years ago
  1. # coding: utf-8
  2. """Utilities for installing server extensions for the notebook"""
  3. # Copyright (c) Jupyter Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from __future__ import print_function
  6. import importlib
  7. import sys
  8. from jupyter_core.paths import jupyter_config_path
  9. from ._version import __version__
  10. from .config_manager import BaseJSONConfigManager
  11. from .extensions import (
  12. BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED, GREEN_OK, RED_X
  13. )
  14. from traitlets import Bool
  15. from traitlets.utils.importstring import import_item
  16. # ------------------------------------------------------------------------------
  17. # Public API
  18. # ------------------------------------------------------------------------------
  19. def toggle_serverextension_python(import_name, enabled=None, parent=None,
  20. user=True, sys_prefix=False, logger=None):
  21. """Toggle a server extension.
  22. By default, toggles the extension in the system-wide Jupyter configuration
  23. location (e.g. /usr/local/etc/jupyter).
  24. Parameters
  25. ----------
  26. import_name : str
  27. Importable Python module (dotted-notation) exposing the magic-named
  28. `load_jupyter_server_extension` function
  29. enabled : bool [default: None]
  30. Toggle state for the extension. Set to None to toggle, True to enable,
  31. and False to disable the extension.
  32. parent : Configurable [default: None]
  33. user : bool [default: True]
  34. Toggle in the user's configuration location (e.g. ~/.jupyter).
  35. sys_prefix : bool [default: False]
  36. Toggle in the current Python environment's configuration location
  37. (e.g. ~/.envs/my-env/etc/jupyter). Will override `user`.
  38. logger : Jupyter logger [optional]
  39. Logger instance to use
  40. """
  41. user = False if sys_prefix else user
  42. config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
  43. cm = BaseJSONConfigManager(parent=parent, config_dir=config_dir)
  44. cfg = cm.get("jupyter_notebook_config")
  45. server_extensions = (
  46. cfg.setdefault("NotebookApp", {})
  47. .setdefault("nbserver_extensions", {})
  48. )
  49. old_enabled = server_extensions.get(import_name, None)
  50. new_enabled = enabled if enabled is not None else not old_enabled
  51. if logger:
  52. if new_enabled:
  53. logger.info(u"Enabling: %s" % (import_name))
  54. else:
  55. logger.info(u"Disabling: %s" % (import_name))
  56. server_extensions[import_name] = new_enabled
  57. if logger:
  58. logger.info(u"- Writing config: {}".format(config_dir))
  59. cm.update("jupyter_notebook_config", cfg)
  60. if new_enabled:
  61. validate_serverextension(import_name, logger)
  62. def validate_serverextension(import_name, logger=None):
  63. """Assess the health of an installed server extension
  64. Returns a list of validation warnings.
  65. Parameters
  66. ----------
  67. import_name : str
  68. Importable Python module (dotted-notation) exposing the magic-named
  69. `load_jupyter_server_extension` function
  70. logger : Jupyter logger [optional]
  71. Logger instance to use
  72. """
  73. warnings = []
  74. infos = []
  75. func = None
  76. if logger:
  77. logger.info(" - Validating...")
  78. try:
  79. mod = importlib.import_module(import_name)
  80. func = getattr(mod, 'load_jupyter_server_extension', None)
  81. version = getattr(mod, '__version__', '')
  82. except Exception:
  83. logger.warning("Error loading server extension %s", import_name)
  84. import_msg = u" {} is {} importable?"
  85. if func is not None:
  86. infos.append(import_msg.format(GREEN_OK, import_name))
  87. else:
  88. warnings.append(import_msg.format(RED_X, import_name))
  89. post_mortem = u" {} {} {}"
  90. if logger:
  91. if warnings:
  92. [logger.info(info) for info in infos]
  93. [logger.warn(warning) for warning in warnings]
  94. else:
  95. logger.info(post_mortem.format(import_name, version, GREEN_OK))
  96. return warnings
  97. # ----------------------------------------------------------------------
  98. # Applications
  99. # ----------------------------------------------------------------------
  100. flags = {}
  101. flags.update(BaseExtensionApp.flags)
  102. flags.pop("y", None)
  103. flags.pop("generate-config", None)
  104. flags.update({
  105. "user" : ({
  106. "ToggleServerExtensionApp" : {
  107. "user" : True,
  108. }}, "Perform the operation for the current user"
  109. ),
  110. "system" : ({
  111. "ToggleServerExtensionApp" : {
  112. "user" : False,
  113. "sys_prefix": False,
  114. }}, "Perform the operation system-wide"
  115. ),
  116. "sys-prefix" : ({
  117. "ToggleServerExtensionApp" : {
  118. "sys_prefix" : True,
  119. }}, "Use sys.prefix as the prefix for installing server extensions"
  120. ),
  121. "py" : ({
  122. "ToggleServerExtensionApp" : {
  123. "python" : True,
  124. }}, "Install from a Python package"
  125. ),
  126. })
  127. flags['python'] = flags['py']
  128. class ToggleServerExtensionApp(BaseExtensionApp):
  129. """A base class for enabling/disabling extensions"""
  130. name = "jupyter serverextension enable/disable"
  131. description = "Enable/disable a server extension using frontend configuration files."
  132. flags = flags
  133. user = Bool(True, config=True, help="Whether to do a user install")
  134. sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
  135. python = Bool(False, config=True, help="Install from a Python package")
  136. def toggle_server_extension(self, import_name):
  137. """Change the status of a named server extension.
  138. Uses the value of `self._toggle_value`.
  139. Parameters
  140. ---------
  141. import_name : str
  142. Importable Python module (dotted-notation) exposing the magic-named
  143. `load_jupyter_server_extension` function
  144. """
  145. toggle_serverextension_python(
  146. import_name, self._toggle_value, parent=self, user=self.user,
  147. sys_prefix=self.sys_prefix, logger=self.log)
  148. def toggle_server_extension_python(self, package):
  149. """Change the status of some server extensions in a Python package.
  150. Uses the value of `self._toggle_value`.
  151. Parameters
  152. ---------
  153. package : str
  154. Importable Python module exposing the
  155. magic-named `_jupyter_server_extension_paths` function
  156. """
  157. m, server_exts = _get_server_extension_metadata(package)
  158. for server_ext in server_exts:
  159. module = server_ext['module']
  160. self.toggle_server_extension(module)
  161. def start(self):
  162. """Perform the App's actions as configured"""
  163. if not self.extra_args:
  164. sys.exit('Please specify a server extension/package to enable or disable')
  165. for arg in self.extra_args:
  166. if self.python:
  167. self.toggle_server_extension_python(arg)
  168. else:
  169. self.toggle_server_extension(arg)
  170. class EnableServerExtensionApp(ToggleServerExtensionApp):
  171. """An App that enables (and validates) Server Extensions"""
  172. name = "jupyter serverextension enable"
  173. description = """
  174. Enable a serverextension in configuration.
  175. Usage
  176. jupyter serverextension enable [--system|--sys-prefix]
  177. """
  178. _toggle_value = True
  179. class DisableServerExtensionApp(ToggleServerExtensionApp):
  180. """An App that disables Server Extensions"""
  181. name = "jupyter serverextension disable"
  182. description = """
  183. Disable a serverextension in configuration.
  184. Usage
  185. jupyter serverextension disable [--system|--sys-prefix]
  186. """
  187. _toggle_value = False
  188. class ListServerExtensionsApp(BaseExtensionApp):
  189. """An App that lists (and validates) Server Extensions"""
  190. name = "jupyter serverextension list"
  191. version = __version__
  192. description = "List all server extensions known by the configuration system"
  193. def list_server_extensions(self):
  194. """List all enabled and disabled server extensions, by config path
  195. Enabled extensions are validated, potentially generating warnings.
  196. """
  197. config_dirs = jupyter_config_path()
  198. for config_dir in config_dirs:
  199. cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
  200. data = cm.get("jupyter_notebook_config")
  201. server_extensions = (
  202. data.setdefault("NotebookApp", {})
  203. .setdefault("nbserver_extensions", {})
  204. )
  205. if server_extensions:
  206. print(u'config dir: {}'.format(config_dir))
  207. for import_name, enabled in server_extensions.items():
  208. print(u' {} {}'.format(
  209. import_name,
  210. GREEN_ENABLED if enabled else RED_DISABLED))
  211. validate_serverextension(import_name, self.log)
  212. def start(self):
  213. """Perform the App's actions as configured"""
  214. self.list_server_extensions()
  215. _examples = """
  216. jupyter serverextension list # list all configured server extensions
  217. jupyter serverextension enable --py <packagename> # enable all server extensions in a Python package
  218. jupyter serverextension disable --py <packagename> # disable all server extensions in a Python package
  219. """
  220. class ServerExtensionApp(BaseExtensionApp):
  221. """Root level server extension app"""
  222. name = "jupyter serverextension"
  223. version = __version__
  224. description = "Work with Jupyter server extensions"
  225. examples = _examples
  226. subcommands = dict(
  227. enable=(EnableServerExtensionApp, "Enable a server extension"),
  228. disable=(DisableServerExtensionApp, "Disable a server extension"),
  229. list=(ListServerExtensionsApp, "List server extensions")
  230. )
  231. def start(self):
  232. """Perform the App's actions as configured"""
  233. super(ServerExtensionApp, self).start()
  234. # The above should have called a subcommand and raised NoStart; if we
  235. # get here, it didn't, so we should self.log.info a message.
  236. subcmds = ", ".join(sorted(self.subcommands))
  237. sys.exit("Please supply at least one subcommand: %s" % subcmds)
  238. main = ServerExtensionApp.launch_instance
  239. # ------------------------------------------------------------------------------
  240. # Private API
  241. # ------------------------------------------------------------------------------
  242. def _get_server_extension_metadata(module):
  243. """Load server extension metadata from a module.
  244. Returns a tuple of (
  245. the package as loaded
  246. a list of server extension specs: [
  247. {
  248. "module": "mockextension"
  249. }
  250. ]
  251. )
  252. Parameters
  253. ----------
  254. module : str
  255. Importable Python module exposing the
  256. magic-named `_jupyter_server_extension_paths` function
  257. """
  258. m = import_item(module)
  259. if not hasattr(m, '_jupyter_server_extension_paths'):
  260. raise KeyError(u'The Python module {} does not include any valid server extensions'.format(module))
  261. return m, m._jupyter_server_extension_paths()
  262. if __name__ == '__main__':
  263. main()