labextensions.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. # coding: utf-8
  2. """Utilities for installing Javascript extensions for JupyterLab"""
  3. # Copyright (c) Jupyter Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from __future__ import print_function
  6. import os
  7. import shutil
  8. import sys
  9. import tarfile
  10. from os.path import join as pjoin, normpath
  11. from jupyter_core.paths import (
  12. jupyter_data_dir, jupyter_config_dir, jupyter_config_path,
  13. SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
  14. )
  15. from ipython_genutils.path import ensure_dir_exists
  16. from ipython_genutils.py3compat import string_types, cast_unicode_py2
  17. from . import __version__
  18. from traitlets.config.manager import BaseJSONConfigManager
  19. from traitlets.utils.importstring import import_item
  20. from tornado.log import LogFormatter
  21. from . import (
  22. get_labextension_manifest_data_by_folder,
  23. )
  24. # Constants for pretty print extension listing function.
  25. # Window doesn't support coloring in the commandline
  26. GREEN_ENABLED = '\033[32m enabled \033[0m' if os.name != 'nt' else 'enabled '
  27. RED_DISABLED = '\033[31mdisabled\033[0m' if os.name != 'nt' else 'disabled'
  28. GREEN_OK = '\033[32mOK\033[0m' if os.name != 'nt' else 'ok'
  29. RED_X = '\033[31m X\033[0m' if os.name != 'nt' else ' X'
  30. #------------------------------------------------------------------------------
  31. # Public API
  32. #------------------------------------------------------------------------------
  33. class ArgumentConflict(ValueError):
  34. pass
  35. def check_labextension(files, user=False, prefix=None, labextensions_dir=None, sys_prefix=False):
  36. """Check whether labextension files have been installed
  37. Returns True if all files are found, False if any are missing.
  38. Parameters
  39. ----------
  40. files : list(paths)
  41. a list of relative paths within labextensions.
  42. user : bool [default: False]
  43. Whether to check the user's .jupyter/labextensions directory.
  44. Otherwise check a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
  45. prefix : str [optional]
  46. Specify install prefix, if it should differ from default (e.g. /usr/local).
  47. Will check prefix/share/jupyter/labextensions
  48. labextensions_dir : str [optional]
  49. Specify absolute path of labextensions directory explicitly.
  50. sys_prefix : bool [default: False]
  51. Install into the sys.prefix, i.e. environment
  52. """
  53. labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
  54. # make sure labextensions dir exists
  55. if not os.path.exists(labext):
  56. return False
  57. if isinstance(files, string_types):
  58. # one file given, turn it into a list
  59. files = [files]
  60. return all(os.path.exists(pjoin(labext, f)) for f in files)
  61. def install_labextension(path, name, overwrite=False, symlink=False,
  62. user=False, prefix=None, labextensions_dir=None,
  63. logger=None, sys_prefix=False
  64. ):
  65. """Install a Javascript extension for JupyterLab
  66. Stages files and/or directories into the labextensions directory.
  67. By default, this compares modification time, and only stages files that need updating.
  68. If `overwrite` is specified, matching files are purged before proceeding.
  69. Parameters
  70. ----------
  71. path : path to file, directory, zip or tarball archive, or URL to install
  72. Archives (zip or tarballs) will be extracted into the labextensions directory.
  73. name : str
  74. name the labextension is installed to. For example, if name is 'foo', then
  75. the source file will be installed to 'labextensions/foo'.
  76. overwrite : bool [default: False]
  77. If True, always install the files, regardless of what may already be installed.
  78. symlink : bool [default: False]
  79. If True, create a symlink in labextensions, rather than copying files.
  80. Not allowed with URLs or archives. Windows support for symlinks requires
  81. Vista or above, Python 3, and a permission bit which only admin users
  82. have by default, so don't rely on it.
  83. user : bool [default: False]
  84. Whether to install to the user's labextensions directory.
  85. Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
  86. prefix : str [optional]
  87. Specify install prefix, if it should differ from default (e.g. /usr/local).
  88. Will install to ``<prefix>/share/jupyter/labextensions``
  89. labextensions_dir : str [optional]
  90. Specify absolute path of labextensions directory explicitly.
  91. logger : Jupyter logger [optional]
  92. Logger instance to use
  93. sys_prefix : bool [default: False]
  94. Install into the sys.prefix, i.e. environment
  95. """
  96. # the actual path to which we eventually installed
  97. full_dest = None
  98. labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
  99. # make sure labextensions dir exists
  100. ensure_dir_exists(labext)
  101. # forcing symlink parameter to False if os.symlink does not exist (e.g., on Windows machines running python 2)
  102. if not hasattr(os, 'symlink'):
  103. symlink = False
  104. if isinstance(path, (list, tuple)):
  105. raise TypeError("path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions")
  106. path = cast_unicode_py2(path)
  107. if path.startswith(('https://', 'http://')):
  108. raise NotImplementedError('Urls are not yet supported for labextensions')
  109. elif path.endswith('.zip') or _safe_is_tarfile(path):
  110. raise NotImplementedError('Archive files are not yet supported for labextensions')
  111. else:
  112. destination = cast_unicode_py2(name)
  113. full_dest = normpath(pjoin(labext, destination))
  114. if overwrite and os.path.lexists(full_dest):
  115. if logger:
  116. logger.info("Removing: %s" % full_dest)
  117. if os.path.isdir(full_dest) and not os.path.islink(full_dest):
  118. shutil.rmtree(full_dest)
  119. else:
  120. os.remove(full_dest)
  121. if symlink:
  122. path = os.path.abspath(path)
  123. if not os.path.exists(full_dest):
  124. if logger:
  125. logger.info("Symlinking: %s -> %s" % (full_dest, path))
  126. os.symlink(path, full_dest)
  127. elif os.path.isdir(path):
  128. path = pjoin(os.path.abspath(path), '') # end in path separator
  129. for parent, dirs, files in os.walk(path):
  130. dest_dir = pjoin(full_dest, parent[len(path):])
  131. if not os.path.exists(dest_dir):
  132. if logger:
  133. logger.info("Making directory: %s" % dest_dir)
  134. os.makedirs(dest_dir)
  135. for file in files:
  136. src = pjoin(parent, file)
  137. dest_file = pjoin(dest_dir, file)
  138. _maybe_copy(src, dest_file, logger=logger)
  139. else:
  140. src = path
  141. _maybe_copy(src, full_dest, logger=logger)
  142. return full_dest
  143. def install_labextension_python(module, overwrite=False, symlink=False,
  144. user=False, sys_prefix=False, prefix=None, labextensions_dir=None, logger=None):
  145. """Install a labextension bundled in a Python package.
  146. Returns a list of installed/updated directories.
  147. See install_labextension for parameter information."""
  148. m, labexts = _get_labextension_metadata(module)
  149. base_path = os.path.split(m.__file__)[0]
  150. full_dests = []
  151. for labext in labexts:
  152. src = os.path.join(base_path, labext['src'])
  153. name = labext['name']
  154. if logger:
  155. logger.info("Installing %s -> %s" % (src, name))
  156. full_dest = install_labextension(
  157. src, name=name, overwrite=overwrite, symlink=symlink,
  158. user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir,
  159. logger=logger
  160. )
  161. validate_labextension_folder(name, full_dest, logger)
  162. full_dests.append(full_dest)
  163. return full_dests
  164. def uninstall_labextension(name, user=False, sys_prefix=False, prefix=None,
  165. labextensions_dir=None, logger=None):
  166. """Uninstall a Javascript extension of JupyterLab
  167. Removes staged files and/or directories in the labextensions directory and
  168. removes the extension from the frontend config.
  169. Parameters
  170. ----------
  171. name: str
  172. The name of the labextension.
  173. user : bool [default: False]
  174. Whether to uninstall from the user's labextensions directory.
  175. Otherwise do a system-wide uninstall (e.g. /usr/local/share/jupyter/labextensions).
  176. sys_prefix : bool [default: False]
  177. Uninstall from the sys.prefix, i.e. environment
  178. prefix : str [optional]
  179. Specify prefix, if it should differ from default (e.g. /usr/local).
  180. Will uninstall from ``<prefix>/share/jupyter/labextensions``
  181. labextensions_dir : str [optional]
  182. Specify absolute path of labextensions directory explicitly.
  183. logger : Jupyter logger [optional]
  184. Logger instance to use
  185. """
  186. labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
  187. dest = cast_unicode_py2(name)
  188. full_dest = pjoin(labext, dest)
  189. if os.path.lexists(full_dest):
  190. if logger:
  191. logger.info("Removing: %s" % full_dest)
  192. if os.path.isdir(full_dest) and not os.path.islink(full_dest):
  193. shutil.rmtree(full_dest)
  194. else:
  195. os.remove(full_dest)
  196. disable_labextension(name, user=user, sys_prefix=sys_prefix,
  197. logger=logger)
  198. def uninstall_labextension_python(module,
  199. user=False, sys_prefix=False, prefix=None, labextensions_dir=None,
  200. logger=None):
  201. """Uninstall a labextension bundled in a Python package.
  202. See parameters of `install_labextension_python`
  203. """
  204. m, labexts = _get_labextension_metadata(module)
  205. for labext in labexts:
  206. name = labext['name']
  207. if logger:
  208. logger.info("Uninstalling {}".format(name))
  209. uninstall_labextension(name, user=user, sys_prefix=sys_prefix,
  210. prefix=prefix, labextensions_dir=labextensions_dir, logger=logger)
  211. def _set_labextension_state(name, state,
  212. user=True, sys_prefix=False, logger=None):
  213. """Set whether the JupyterLab frontend should use the named labextension
  214. Returns True if the final state is the one requested.
  215. Parameters
  216. name : string
  217. The name of the extension.
  218. state : bool
  219. The state in which to leave the extension
  220. user : bool [default: True]
  221. Whether to update the user's .jupyter/labextensions directory
  222. sys_prefix : bool [default: False]
  223. Whether to update the sys.prefix, i.e. environment. Will override
  224. `user`.
  225. logger : Jupyter logger [optional]
  226. Logger instance to use
  227. """
  228. user = False if sys_prefix else user
  229. config_dir = os.path.join(
  230. _get_config_dir(user=user, sys_prefix=sys_prefix), 'labconfig')
  231. cm = BaseJSONConfigManager(config_dir=config_dir)
  232. if logger:
  233. logger.info("{} extension {}...".format(
  234. "Enabling" if state else "Disabling",
  235. name
  236. ))
  237. cfg = cm.get("jupyterlab_config")
  238. labextensions = (
  239. cfg.setdefault("LabApp", {})
  240. .setdefault("labextensions", {})
  241. )
  242. old_enabled = labextensions.get(name, None)
  243. new_enabled = state if state is not None else not old_enabled
  244. if logger:
  245. if new_enabled:
  246. logger.info(u"Enabling: %s" % (name))
  247. else:
  248. logger.info(u"Disabling: %s" % (name))
  249. labextensions[name] = new_enabled
  250. if logger:
  251. logger.info(u"- Writing config: {}".format(config_dir))
  252. cm.update("jupyterlab_config", cfg)
  253. if new_enabled:
  254. validate_labextension(name, logger=logger)
  255. return old_enabled == state
  256. def _set_labextension_state_python(state, module, user, sys_prefix,
  257. logger=None):
  258. """Enable or disable some labextensions stored in a Python package
  259. Returns a list of whether the state was achieved (i.e. changed, or was
  260. already right)
  261. Parameters
  262. ----------
  263. state : Bool
  264. Whether the extensions should be enabled
  265. module : str
  266. Importable Python module exposing the
  267. magic-named `_jupyter_labextension_paths` function
  268. user : bool
  269. Whether to enable in the user's labextensions directory.
  270. sys_prefix : bool
  271. Enable/disable in the sys.prefix, i.e. environment
  272. logger : Jupyter logger [optional]
  273. Logger instance to use
  274. """
  275. m, labexts = _get_labextension_metadata(module)
  276. return [_set_labextension_state(name=labext["name"],
  277. state=state,
  278. user=user, sys_prefix=sys_prefix,
  279. logger=logger)
  280. for labext in labexts]
  281. def enable_labextension(name, user=True, sys_prefix=False,
  282. logger=None):
  283. """Enable a named labextension
  284. Returns True if the final state is the one requested.
  285. Parameters
  286. ----------
  287. name : string
  288. The name of the extension.
  289. user : bool [default: True]
  290. Whether to enable in the user's labextensions directory.
  291. sys_prefix : bool [default: False]
  292. Whether to enable in the sys.prefix, i.e. environment. Will override
  293. `user`
  294. logger : Jupyter logger [optional]
  295. Logger instance to use
  296. """
  297. return _set_labextension_state(name=name,
  298. state=True,
  299. user=user, sys_prefix=sys_prefix,
  300. logger=logger)
  301. def disable_labextension(name, user=True, sys_prefix=False,
  302. logger=None):
  303. """Disable a named labextension
  304. Returns True if the final state is the one requested.
  305. Parameters
  306. ----------
  307. name : string
  308. The name of the extension.
  309. user : bool [default: True]
  310. Whether to enable in the user's labextensions directory.
  311. sys_prefix : bool [default: False]
  312. Whether to enable in the sys.prefix, i.e. environment. Will override
  313. `user`.
  314. logger : Jupyter logger [optional]
  315. Logger instance to use
  316. """
  317. return _set_labextension_state(name=name,
  318. state=False,
  319. user=user, sys_prefix=sys_prefix,
  320. logger=logger)
  321. def enable_labextension_python(module, user=True, sys_prefix=False,
  322. logger=None):
  323. """Enable some labextensions associated with a Python module.
  324. Returns a list of whether the state was achieved (i.e. changed, or was
  325. already right)
  326. Parameters
  327. ----------
  328. module : str
  329. Importable Python module exposing the
  330. magic-named `_jupyter_labextension_paths` function
  331. user : bool [default: True]
  332. Whether to enable in the user's labextensions directory.
  333. sys_prefix : bool [default: False]
  334. Whether to enable in the sys.prefix, i.e. environment. Will override
  335. `user`
  336. logger : Jupyter logger [optional]
  337. Logger instance to use
  338. """
  339. return _set_labextension_state_python(True, module, user, sys_prefix,
  340. logger=logger)
  341. def disable_labextension_python(module, user=True, sys_prefix=False,
  342. logger=None):
  343. """Disable some labextensions associated with a Python module.
  344. Returns True if the final state is the one requested.
  345. Parameters
  346. ----------
  347. module : str
  348. Importable Python module exposing the
  349. magic-named `_jupyter_labextension_paths` function
  350. user : bool [default: True]
  351. Whether to enable in the user's labextensions directory.
  352. sys_prefix : bool [default: False]
  353. Whether to enable in the sys.prefix, i.e. environment
  354. logger : Jupyter logger [optional]
  355. Logger instance to use
  356. """
  357. return _set_labextension_state_python(False, module, user, sys_prefix,
  358. logger=logger)
  359. def validate_labextension(name, logger=None):
  360. """Validate a named labextension.
  361. Looks across all of the labextension directories.
  362. Returns a list of warnings.
  363. Parameters
  364. ----------
  365. name : str
  366. The name of the extension.
  367. logger : Jupyter logger [optional]
  368. Logger instance to use
  369. """
  370. for exts in _labextension_dirs():
  371. full_dest = os.path.join(exts, name)
  372. if os.path.exists(full_dest):
  373. return validate_labextension_folder(name, full_dest, logger)
  374. def validate_labextension_folder(name, full_dest, logger=None):
  375. """Assess the health of an installed labextension
  376. Returns a list of warnings.
  377. Parameters
  378. ----------
  379. full_dest : str
  380. The on-disk location of the installed labextension: this should end
  381. with `labextensions/<name>`
  382. logger : Jupyter logger [optional]
  383. Logger instance to use
  384. """
  385. if logger:
  386. logger.info(" - Validating...")
  387. infos = []
  388. warnings = []
  389. hasFiles = True
  390. hasEntry = False
  391. data = get_labextension_manifest_data_by_folder(full_dest)
  392. for manifest in data.values():
  393. if ('entry' in manifest and 'modules' in manifest):
  394. if (manifest['entry'] in manifest['modules']):
  395. hasEntry = True
  396. files = manifest.get('files', [])
  397. if not files:
  398. hasFiles = False
  399. for fname in files:
  400. path = os.path.join(full_dest, fname)
  401. if not os.path.exists(path):
  402. hasFiles = False
  403. entry_msg = u" {} has {} entry point?"
  404. name = os.path.basename(full_dest)
  405. if hasEntry:
  406. (entry_msg.format(GREEN_OK, name))
  407. else:
  408. warnings.append(entry_msg.format(RED_X, name))
  409. files_msg = u" {} has necessary files?"
  410. if hasFiles:
  411. infos.append(files_msg.format(GREEN_OK, name))
  412. else:
  413. warnings.append(files_msg.format(RED_X, name))
  414. post_mortem = u" {} {} {}"
  415. if logger:
  416. if warnings:
  417. [logger.info(info) for info in infos]
  418. [logger.warn(warning) for warning in warnings]
  419. else:
  420. logger.info(post_mortem.format(name, "", GREEN_OK))
  421. return warnings
  422. #----------------------------------------------------------------------
  423. # Applications
  424. #----------------------------------------------------------------------
  425. from traitlets import Bool, Unicode
  426. from jupyter_core.application import JupyterApp
  427. _base_flags = {}
  428. _base_flags.update(JupyterApp.flags)
  429. _base_flags.pop("y", None)
  430. _base_flags.pop("generate-config", None)
  431. _base_flags.update({
  432. "user" : ({
  433. "BaseLabExtensionApp" : {
  434. "user" : True,
  435. }}, "Apply the operation only for the given user"
  436. ),
  437. "system" : ({
  438. "BaseLabExtensionApp" : {
  439. "user" : False,
  440. "sys_prefix": False,
  441. }}, "Apply the operation system-wide"
  442. ),
  443. "sys-prefix" : ({
  444. "BaseLabExtensionApp" : {
  445. "sys_prefix" : True,
  446. }}, "Use sys.prefix as the prefix for installing labextensions (for environments, packaging)"
  447. ),
  448. "py" : ({
  449. "BaseLabExtensionApp" : {
  450. "python" : True,
  451. }}, "Install from a Python package"
  452. )
  453. })
  454. _base_flags['python'] = _base_flags['py']
  455. class BaseLabExtensionApp(JupyterApp):
  456. """Base labextension installer app"""
  457. _log_formatter_cls = LogFormatter
  458. flags = _base_flags
  459. version = __version__
  460. user = Bool(False, config=True, help="Whether to do a user install")
  461. sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
  462. python = Bool(False, config=True, help="Install from a Python package")
  463. def _log_format_default(self):
  464. """A default format for messages"""
  465. return "%(message)s"
  466. flags = {}
  467. flags.update(_base_flags)
  468. flags.update({
  469. "overwrite" : ({
  470. "InstallLabExtensionApp" : {
  471. "overwrite" : True,
  472. }}, "Force overwrite of existing files"
  473. ),
  474. "symlink" : ({
  475. "InstallLabExtensionApp" : {
  476. "symlink" : True,
  477. }}, "Create symlink instead of copying files"
  478. ),
  479. })
  480. flags['s'] = flags['symlink']
  481. aliases = {
  482. "prefix" : "InstallLabExtensionApp.prefix",
  483. "labextensions" : "InstallLabExtensionApp.labextensions_dir",
  484. }
  485. class InstallLabExtensionApp(BaseLabExtensionApp):
  486. """Entry point for installing JupyterLab extensions"""
  487. description = """Install JupyterLab extensions
  488. Usage
  489. jupyter labextension install /path/to/myextension myextension [--user|--sys-prefix]
  490. jupyter labextension install --py myextensionPyPackage [--user|--sys-prefix]
  491. This copies a file or a folder into the Jupyter labextensions directory.
  492. If a URL is given, it will be downloaded.
  493. If an archive is given, it will be extracted into labextensions.
  494. If the requested files are already up to date, no action is taken
  495. unless --overwrite is specified.
  496. """
  497. examples = """
  498. jupyter labextension install /path/to/myextension myextension
  499. jupyter labextension install --py myextensionPyPackage
  500. """
  501. aliases = aliases
  502. flags = flags
  503. overwrite = Bool(False, config=True, help="Force overwrite of existing files")
  504. symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
  505. prefix = Unicode('', config=True, help="Installation prefix")
  506. labextensions_dir = Unicode('', config=True,
  507. help="Full path to labextensions dir (probably use prefix or user)")
  508. def _config_file_name_default(self):
  509. """The default config file name."""
  510. return 'jupyterlab_config'
  511. def install_extensions(self):
  512. """Perform the installation of labextension(s)"""
  513. if self.python:
  514. if len(self.extra_args) > 1:
  515. raise ValueError("Only one labextension allowed at a time. "
  516. "Call multiple times to install multiple extensions.")
  517. install = install_labextension_python
  518. kwargs = {}
  519. else:
  520. if len(self.extra_args) > 2:
  521. raise ValueError("Only one labextension allowed at a time. "
  522. "Call multiple times to install multiple extensions.")
  523. install = install_labextension
  524. kwargs = {'name': self.extra_args[1]}
  525. full_dests = install(self.extra_args[0],
  526. overwrite=self.overwrite,
  527. symlink=self.symlink,
  528. user=self.user,
  529. sys_prefix=self.sys_prefix,
  530. prefix=self.prefix,
  531. labextensions_dir=self.labextensions_dir,
  532. logger=self.log,
  533. **kwargs
  534. )
  535. if full_dests:
  536. self.log.info(
  537. u"\nTo enable this labextension in the browser every time"
  538. " JupyterLab loads:\n\n"
  539. " jupyter labextension enable {}{}{}{}\n".format(
  540. self.extra_args[0] if self.python else self.extra_args[1],
  541. " --user" if self.user else "",
  542. " --py" if self.python else "",
  543. " --sys-prefix" if self.sys_prefix else ""
  544. )
  545. )
  546. def start(self):
  547. """Perform the App's function as configured"""
  548. if not self.extra_args:
  549. sys.exit('Please specify a labextension to install')
  550. else:
  551. try:
  552. self.install_extensions()
  553. except ArgumentConflict as e:
  554. sys.exit(str(e))
  555. class UninstallLabExtensionApp(BaseLabExtensionApp):
  556. """Entry point for uninstalling JupyterLab extensions"""
  557. version = __version__
  558. description = """Uninstall Jupyterlab extensions
  559. Usage
  560. jupyter labextension uninstall myextension
  561. jupyter labextension uninstall --py myextensionPyPackage
  562. This uninstalls a labextension.
  563. """
  564. examples = """
  565. jupyter labextension uninstall myextension
  566. jupyter labextension uninstall --py myextensionPyPackage
  567. """
  568. aliases = {
  569. "prefix" : "UninstallLabExtensionApp.prefix",
  570. "labextensions" : "UninstallLabExtensionApp.labextensions_dir",
  571. "name": "UninstallLabExtensionApp.name",
  572. }
  573. prefix = Unicode('', config=True, help="Installation prefix")
  574. labextensions_dir = Unicode('', config=True, help="Full path to labextensions dir (probably use prefix or user)")
  575. name = Unicode('', config=True, help="The name of the extension.")
  576. def _config_file_name_default(self):
  577. """The default config file name."""
  578. return 'jupyterlab_config'
  579. def uninstall_extensions(self):
  580. """Uninstall some labextensions"""
  581. kwargs = {
  582. 'user': self.user,
  583. 'sys_prefix': self.sys_prefix,
  584. 'prefix': self.prefix,
  585. 'labextensions_dir': self.labextensions_dir,
  586. 'logger': self.log
  587. }
  588. arg_count = 1
  589. if len(self.extra_args) > arg_count:
  590. raise ValueError("only one labextension allowed at a time. Call multiple times to uninstall multiple extensions.")
  591. if len(self.extra_args) < arg_count:
  592. raise ValueError("not enough arguments")
  593. if self.python:
  594. uninstall_labextension_python(self.extra_args[0], **kwargs)
  595. else:
  596. uninstall_labextension(self.extra_args[0], **kwargs)
  597. def start(self):
  598. if not self.extra_args:
  599. sys.exit('Please specify a labextension to uninstall')
  600. else:
  601. try:
  602. self.uninstall_extensions()
  603. except ArgumentConflict as e:
  604. sys.exit(str(e))
  605. class ToggleLabExtensionApp(BaseLabExtensionApp):
  606. """A base class for apps that enable/disable extensions"""
  607. name = "jupyter labextension enable/disable"
  608. version = __version__
  609. description = "Enable/disable a labextension in configuration."
  610. user = Bool(True, config=True, help="Apply the configuration only for the current user (default)")
  611. _toggle_value = None
  612. def _config_file_name_default(self):
  613. """The default config file name."""
  614. return 'jupyterlab_config'
  615. def toggle_labextension_python(self, module):
  616. """Toggle some extensions in an importable Python module.
  617. Returns a list of booleans indicating whether the state was changed as
  618. requested.
  619. Parameters
  620. ----------
  621. module : str
  622. Importable Python module exposing the
  623. magic-named `_jupyter_labextension_paths` function
  624. """
  625. toggle = (enable_labextension_python if self._toggle_value
  626. else disable_labextension_python)
  627. return toggle(module,
  628. user=self.user,
  629. sys_prefix=self.sys_prefix,
  630. logger=self.log)
  631. def toggle_labextension(self, name):
  632. """Toggle some a named labextension by require-able AMD module.
  633. Returns whether the state was changed as requested.
  634. Parameters
  635. ----------
  636. require : str
  637. require.js path used to load the labextension
  638. """
  639. toggle = (enable_labextension if self._toggle_value
  640. else disable_labextension)
  641. return toggle(name,
  642. user=self.user, sys_prefix=self.sys_prefix,
  643. logger=self.log)
  644. def start(self):
  645. if not self.extra_args:
  646. sys.exit('Please specify a labextension/package to enable or disable')
  647. elif len(self.extra_args) > 1:
  648. sys.exit('Please specify one labextension/package at a time')
  649. if self.python:
  650. self.toggle_labextension_python(self.extra_args[0])
  651. else:
  652. self.toggle_labextension(self.extra_args[0])
  653. class EnableLabExtensionApp(ToggleLabExtensionApp):
  654. """An App that enables labextensions"""
  655. name = "jupyter labextension enable"
  656. description = """
  657. Enable a labextension in frontend configuration.
  658. Usage
  659. jupyter labextension enable myextension [--system|--sys-prefix]
  660. """
  661. _toggle_value = True
  662. class DisableLabExtensionApp(ToggleLabExtensionApp):
  663. """An App that disables labextensions"""
  664. name = "jupyter labextension disable"
  665. description = """
  666. Enable a labextension in frontend configuration.
  667. Usage
  668. jupyter labextension disable myextension [--system|--sys-prefix]
  669. """
  670. _toggle_value = None
  671. class ListLabExtensionsApp(BaseLabExtensionApp):
  672. """An App that lists and validates labextensions"""
  673. name = "jupyter labextension list"
  674. version = __version__
  675. description = "List all labextensions known by the configuration system"
  676. def list_labextensions(self):
  677. """List all the labextensions"""
  678. config_dirs = [os.path.join(p, 'labconfig') for p in jupyter_config_path()]
  679. print("Known labextensions:")
  680. for config_dir in config_dirs:
  681. cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
  682. data = cm.get("jupyterlab_config")
  683. labextensions = (
  684. data.setdefault("LabApp", {})
  685. .setdefault("labextensions", {})
  686. )
  687. if labextensions:
  688. print(u'config dir: {}'.format(config_dir))
  689. for name, enabled in labextensions.items():
  690. print(u' {} {}'.format(
  691. name,
  692. GREEN_ENABLED if enabled else RED_DISABLED))
  693. validate_labextension(name, self.log)
  694. def start(self):
  695. """Perform the App's functions as configured"""
  696. self.list_labextensions()
  697. _examples = """
  698. jupyter labextension list # list all configured labextensions
  699. jupyter labextension install --py <packagename> # install a labextension from a Python package
  700. jupyter labextension enable --py <packagename> # enable all labextensions in a Python package
  701. jupyter labextension disable --py <packagename> # disable all labextensions in a Python package
  702. jupyter labextension uninstall --py <packagename> # uninstall a labextension in a Python package
  703. """
  704. class LabExtensionApp(BaseLabExtensionApp):
  705. """Base jupyter labextension command entry point"""
  706. name = "jupyter labextension"
  707. version = __version__
  708. description = "Work with JupyterLab extensions"
  709. examples = _examples
  710. subcommands = dict(
  711. install=(InstallLabExtensionApp, "Install a labextension"),
  712. enable=(EnableLabExtensionApp, "Enable a labextension"),
  713. disable=(DisableLabExtensionApp, "Disable a labextension"),
  714. uninstall=(UninstallLabExtensionApp, "Uninstall a labextension"),
  715. list=(ListLabExtensionsApp, "List labextensions")
  716. )
  717. def start(self):
  718. """Perform the App's functions as configured"""
  719. super(LabExtensionApp, self).start()
  720. # The above should have called a subcommand and raised NoStart; if we
  721. # get here, it didn't, so we should self.log.info a message.
  722. subcmds = ", ".join(sorted(self.subcommands))
  723. sys.exit("Please supply at least one subcommand: %s" % subcmds)
  724. main = LabExtensionApp.launch_instance
  725. #------------------------------------------------------------------------------
  726. # Private API
  727. #------------------------------------------------------------------------------
  728. def _should_copy(src, dest, logger=None):
  729. """Should a file be copied, if it doesn't exist, or is newer?
  730. Returns whether the file needs to be updated.
  731. Parameters
  732. ----------
  733. src : string
  734. A path that should exist from which to copy a file
  735. src : string
  736. A path that might exist to which to copy a file
  737. logger : Jupyter logger [optional]
  738. Logger instance to use
  739. """
  740. if not os.path.exists(dest):
  741. return True
  742. if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6:
  743. # we add a fudge factor to work around a bug in python 2.x
  744. # that was fixed in python 3.x: http://bugs.python.org/issue12904
  745. if logger:
  746. logger.warn("Out of date: %s" % dest)
  747. return True
  748. if logger:
  749. logger.info("Up to date: %s" % dest)
  750. return False
  751. def _maybe_copy(src, dest, logger=None):
  752. """Copy a file if it needs updating.
  753. Parameters
  754. ----------
  755. src : string
  756. A path that should exist from which to copy a file
  757. src : string
  758. A path that might exist to which to copy a file
  759. logger : Jupyter logger [optional]
  760. Logger instance to use
  761. """
  762. if _should_copy(src, dest, logger=logger):
  763. if logger:
  764. logger.info("Copying: %s -> %s" % (src, dest))
  765. shutil.copy2(src, dest)
  766. def _safe_is_tarfile(path):
  767. """Safe version of is_tarfile, return False on IOError.
  768. Returns whether the file exists and is a tarfile.
  769. Parameters
  770. ----------
  771. path : string
  772. A path that might not exist and or be a tarfile
  773. """
  774. try:
  775. return tarfile.is_tarfile(path)
  776. except IOError:
  777. return False
  778. def _get_labextension_dir(user=False, sys_prefix=False, prefix=None, labextensions_dir=None):
  779. """Return the labextension directory specified
  780. Parameters
  781. ----------
  782. user : bool [default: False]
  783. Get the user's .jupyter/labextensions directory
  784. sys_prefix : bool [default: False]
  785. Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/labextensions
  786. prefix : str [optional]
  787. Get custom prefix
  788. labextensions_dir : str [optional]
  789. Get what you put in
  790. """
  791. if sum(map(bool, [user, prefix, labextensions_dir, sys_prefix])) > 1:
  792. raise ArgumentConflict("cannot specify more than one of user, sys_prefix, prefix, or labextensions_dir")
  793. if user:
  794. labext = pjoin(jupyter_data_dir(), u'labextensions')
  795. elif sys_prefix:
  796. labext = pjoin(ENV_JUPYTER_PATH[0], u'labextensions')
  797. elif prefix:
  798. labext = pjoin(prefix, 'share', 'jupyter', 'labextensions')
  799. elif labextensions_dir:
  800. labext = labextensions_dir
  801. else:
  802. labext = pjoin(SYSTEM_JUPYTER_PATH[0], 'labextensions')
  803. return labext
  804. def _labextension_dirs():
  805. """The possible locations of labextensions.
  806. Returns a list of known base extension locations
  807. """
  808. return [
  809. pjoin(jupyter_data_dir(), u'labextensions'),
  810. pjoin(ENV_JUPYTER_PATH[0], u'labextensions'),
  811. pjoin(SYSTEM_JUPYTER_PATH[0], 'labextensions')
  812. ]
  813. def _get_config_dir(user=False, sys_prefix=False):
  814. """Get the location of config files for the current context
  815. Returns the string to the enviornment
  816. Parameters
  817. ----------
  818. user : bool [default: False]
  819. Get the user's .jupyter config directory
  820. sys_prefix : bool [default: False]
  821. Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
  822. """
  823. user = False if sys_prefix else user
  824. if user and sys_prefix:
  825. raise ArgumentConflict("Cannot specify more than one of user or sys_prefix")
  826. if user:
  827. labext = jupyter_config_dir()
  828. elif sys_prefix:
  829. labext = ENV_CONFIG_PATH[0]
  830. else:
  831. labext = SYSTEM_CONFIG_PATH[0]
  832. return labext
  833. def _get_labextension_metadata(module):
  834. """Get the list of labextension paths associated with a Python module.
  835. Returns a tuple of (the module, [{
  836. 'name': 'mockextension',
  837. 'src': 'static',
  838. }])
  839. Parameters
  840. ----------
  841. module : str
  842. Importable Python module exposing the
  843. magic-named `_jupyter_labextension_paths` function
  844. """
  845. m = import_item(module)
  846. if not hasattr(m, '_jupyter_labextension_paths'):
  847. raise KeyError('The Python module {} is not a valid labextension'.format(module))
  848. labexts = m._jupyter_labextension_paths()
  849. return m, labexts
  850. def _read_config_data(user=False, sys_prefix=False):
  851. """Get the config for the current context
  852. Returns the string to the enviornment
  853. Parameters
  854. ----------
  855. user : bool [default: False]
  856. Get the user's .jupyter config directory
  857. sys_prefix : bool [default: False]
  858. Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
  859. """
  860. config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
  861. config_man = BaseJSONConfigManager(config_dir=config_dir)
  862. return config_man.get('jupyterlab_config')
  863. def _write_config_data(data, user=False, sys_prefix=False):
  864. """Update the config for the current context
  865. Parameters
  866. ----------
  867. data : object
  868. An object which can be accepted by ConfigManager.update
  869. user : bool [default: False]
  870. Get the user's .jupyter config directory
  871. sys_prefix : bool [default: False]
  872. Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
  873. """
  874. config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
  875. config_man = BaseJSONConfigManager(config_dir=config_dir)
  876. config_man.update('jupyterlab_config', data)
  877. if __name__ == '__main__':
  878. main()