1"""Utilities for installing Javascript extensions for the notebook""" 2 3# Copyright (c) Jupyter Development Team. 4# Distributed under the terms of the Modified BSD License. 5 6import os 7import shutil 8import sys 9import tarfile 10import zipfile 11from os.path import basename, join as pjoin, normpath 12 13from urllib.parse import urlparse 14from urllib.request import urlretrieve 15from jupyter_core.paths import ( 16 jupyter_data_dir, jupyter_config_path, jupyter_path, 17 SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, 18) 19from jupyter_core.utils import ensure_dir_exists 20from ipython_genutils.py3compat import string_types, cast_unicode_py2 21from ipython_genutils.tempdir import TemporaryDirectory 22from ._version import __version__ 23from .config_manager import BaseJSONConfigManager 24 25from traitlets.utils.importstring import import_item 26 27DEPRECATED_ARGUMENT = object() 28 29NBCONFIG_SECTIONS = ['common', 'notebook', 'tree', 'edit', 'terminal'] 30 31 32#------------------------------------------------------------------------------ 33# Public API 34#------------------------------------------------------------------------------ 35 36def check_nbextension(files, user=False, prefix=None, nbextensions_dir=None, sys_prefix=False): 37 """Check whether nbextension files have been installed 38 39 Returns True if all files are found, False if any are missing. 40 41 Parameters 42 ---------- 43 44 files : list(paths) 45 a list of relative paths within nbextensions. 46 user : bool [default: False] 47 Whether to check the user's .jupyter/nbextensions directory. 48 Otherwise check a system-wide install (e.g. /usr/local/share/jupyter/nbextensions). 49 prefix : str [optional] 50 Specify install prefix, if it should differ from default (e.g. /usr/local). 51 Will check prefix/share/jupyter/nbextensions 52 nbextensions_dir : str [optional] 53 Specify absolute path of nbextensions directory explicitly. 54 sys_prefix : bool [default: False] 55 Install into the sys.prefix, i.e. environment 56 """ 57 nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir) 58 # make sure nbextensions dir exists 59 if not os.path.exists(nbext): 60 return False 61 62 if isinstance(files, string_types): 63 # one file given, turn it into a list 64 files = [files] 65 66 return all(os.path.exists(pjoin(nbext, f)) for f in files) 67 68 69def install_nbextension(path, overwrite=False, symlink=False, 70 user=False, prefix=None, nbextensions_dir=None, 71 destination=None, verbose=DEPRECATED_ARGUMENT, 72 logger=None, sys_prefix=False 73 ): 74 """Install a Javascript extension for the notebook 75 76 Stages files and/or directories into the nbextensions directory. 77 By default, this compares modification time, and only stages files that need updating. 78 If `overwrite` is specified, matching files are purged before proceeding. 79 80 Parameters 81 ---------- 82 83 path : path to file, directory, zip or tarball archive, or URL to install 84 By default, the file will be installed with its base name, so '/path/to/foo' 85 will install to 'nbextensions/foo'. See the destination argument below to change this. 86 Archives (zip or tarballs) will be extracted into the nbextensions directory. 87 overwrite : bool [default: False] 88 If True, always install the files, regardless of what may already be installed. 89 symlink : bool [default: False] 90 If True, create a symlink in nbextensions, rather than copying files. 91 Not allowed with URLs or archives. Windows support for symlinks requires 92 Vista or above, Python 3, and a permission bit which only admin users 93 have by default, so don't rely on it. 94 user : bool [default: False] 95 Whether to install to the user's nbextensions directory. 96 Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions). 97 prefix : str [optional] 98 Specify install prefix, if it should differ from default (e.g. /usr/local). 99 Will install to ``<prefix>/share/jupyter/nbextensions`` 100 nbextensions_dir : str [optional] 101 Specify absolute path of nbextensions directory explicitly. 102 destination : str [optional] 103 name the nbextension is installed to. For example, if destination is 'foo', then 104 the source file will be installed to 'nbextensions/foo', regardless of the source name. 105 This cannot be specified if an archive is given as the source. 106 logger : Jupyter logger [optional] 107 Logger instance to use 108 """ 109 if verbose != DEPRECATED_ARGUMENT: 110 import warnings 111 warnings.warn("`install_nbextension`'s `verbose` parameter is deprecated, it will have no effects and will be removed in Notebook 5.0", DeprecationWarning) 112 113 # the actual path to which we eventually installed 114 full_dest = None 115 116 nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir) 117 # make sure nbextensions dir exists 118 ensure_dir_exists(nbext) 119 120 # forcing symlink parameter to False if os.symlink does not exist (e.g., on Windows machines running python 2) 121 if not hasattr(os, 'symlink'): 122 symlink = False 123 124 if isinstance(path, (list, tuple)): 125 raise TypeError("path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions") 126 127 path = cast_unicode_py2(path) 128 129 if path.startswith(('https://', 'http://')): 130 if symlink: 131 raise ValueError("Cannot symlink from URLs") 132 # Given a URL, download it 133 with TemporaryDirectory() as td: 134 filename = urlparse(path).path.split('/')[-1] 135 local_path = os.path.join(td, filename) 136 if logger: 137 logger.info("Downloading: %s -> %s" % (path, local_path)) 138 urlretrieve(path, local_path) 139 # now install from the local copy 140 full_dest = install_nbextension(local_path, overwrite=overwrite, symlink=symlink, 141 nbextensions_dir=nbext, destination=destination, logger=logger) 142 elif path.endswith('.zip') or _safe_is_tarfile(path): 143 if symlink: 144 raise ValueError("Cannot symlink from archives") 145 if destination: 146 raise ValueError("Cannot give destination for archives") 147 if logger: 148 logger.info("Extracting: %s -> %s" % (path, nbext)) 149 150 if path.endswith('.zip'): 151 archive = zipfile.ZipFile(path) 152 elif _safe_is_tarfile(path): 153 archive = tarfile.open(path) 154 archive.extractall(nbext) 155 archive.close() 156 # TODO: what to do here 157 full_dest = None 158 else: 159 if not destination: 160 destination = basename(normpath(path)) 161 destination = cast_unicode_py2(destination) 162 full_dest = normpath(pjoin(nbext, destination)) 163 if overwrite and os.path.lexists(full_dest): 164 if logger: 165 logger.info("Removing: %s" % full_dest) 166 if os.path.isdir(full_dest) and not os.path.islink(full_dest): 167 shutil.rmtree(full_dest) 168 else: 169 os.remove(full_dest) 170 171 if symlink: 172 path = os.path.abspath(path) 173 if not os.path.exists(full_dest): 174 if logger: 175 logger.info("Symlinking: %s -> %s" % (full_dest, path)) 176 os.symlink(path, full_dest) 177 elif os.path.isdir(path): 178 path = pjoin(os.path.abspath(path), '') # end in path separator 179 for parent, dirs, files in os.walk(path): 180 dest_dir = pjoin(full_dest, parent[len(path):]) 181 if not os.path.exists(dest_dir): 182 if logger: 183 logger.info("Making directory: %s" % dest_dir) 184 os.makedirs(dest_dir) 185 for file_name in files: 186 src = pjoin(parent, file_name) 187 dest_file = pjoin(dest_dir, file_name) 188 _maybe_copy(src, dest_file, logger=logger) 189 else: 190 src = path 191 _maybe_copy(src, full_dest, logger=logger) 192 193 return full_dest 194 195 196def install_nbextension_python(module, overwrite=False, symlink=False, 197 user=False, sys_prefix=False, prefix=None, nbextensions_dir=None, logger=None): 198 """Install an nbextension bundled in a Python package. 199 200 Returns a list of installed/updated directories. 201 202 See install_nbextension for parameter information.""" 203 m, nbexts = _get_nbextension_metadata(module) 204 base_path = os.path.split(m.__file__)[0] 205 206 full_dests = [] 207 208 for nbext in nbexts: 209 src = os.path.join(base_path, nbext['src']) 210 dest = nbext['dest'] 211 212 if logger: 213 logger.info("Installing %s -> %s" % (src, dest)) 214 full_dest = install_nbextension( 215 src, overwrite=overwrite, symlink=symlink, 216 user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir, 217 destination=dest, logger=logger 218 ) 219 validate_nbextension_python(nbext, full_dest, logger) 220 full_dests.append(full_dest) 221 222 return full_dests 223 224 225def uninstall_nbextension(dest, require=None, user=False, sys_prefix=False, prefix=None, 226 nbextensions_dir=None, logger=None): 227 """Uninstall a Javascript extension of the notebook 228 229 Removes staged files and/or directories in the nbextensions directory and 230 removes the extension from the frontend config. 231 232 Parameters 233 ---------- 234 235 dest : str 236 path to file, directory, zip or tarball archive, or URL to install 237 name the nbextension is installed to. For example, if destination is 'foo', then 238 the source file will be installed to 'nbextensions/foo', regardless of the source name. 239 This cannot be specified if an archive is given as the source. 240 require : str [optional] 241 require.js path used to load the extension. 242 If specified, frontend config loading extension will be removed. 243 user : bool [default: False] 244 Whether to install to the user's nbextensions directory. 245 Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions). 246 prefix : str [optional] 247 Specify install prefix, if it should differ from default (e.g. /usr/local). 248 Will install to ``<prefix>/share/jupyter/nbextensions`` 249 nbextensions_dir : str [optional] 250 Specify absolute path of nbextensions directory explicitly. 251 logger : Jupyter logger [optional] 252 Logger instance to use 253 """ 254 nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir) 255 dest = cast_unicode_py2(dest) 256 full_dest = pjoin(nbext, dest) 257 if os.path.lexists(full_dest): 258 if logger: 259 logger.info("Removing: %s" % full_dest) 260 if os.path.isdir(full_dest) and not os.path.islink(full_dest): 261 shutil.rmtree(full_dest) 262 else: 263 os.remove(full_dest) 264 265 # Look through all of the config sections making sure that the nbextension 266 # doesn't exist. 267 config_dir = os.path.join(_get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig') 268 cm = BaseJSONConfigManager(config_dir=config_dir) 269 if require: 270 for section in NBCONFIG_SECTIONS: 271 cm.update(section, {"load_extensions": {require: None}}) 272 273 274def _find_uninstall_nbextension(filename, logger=None): 275 """Remove nbextension files from the first location they are found. 276 277 Returns True if files were removed, False otherwise. 278 """ 279 filename = cast_unicode_py2(filename) 280 for nbext in jupyter_path('nbextensions'): 281 path = pjoin(nbext, filename) 282 if os.path.lexists(path): 283 if logger: 284 logger.info("Removing: %s" % path) 285 if os.path.isdir(path) and not os.path.islink(path): 286 shutil.rmtree(path) 287 else: 288 os.remove(path) 289 return True 290 291 return False 292 293 294def uninstall_nbextension_python(module, 295 user=False, sys_prefix=False, prefix=None, nbextensions_dir=None, 296 logger=None): 297 """Uninstall an nbextension bundled in a Python package. 298 299 See parameters of `install_nbextension_python` 300 """ 301 m, nbexts = _get_nbextension_metadata(module) 302 for nbext in nbexts: 303 dest = nbext['dest'] 304 require = nbext['require'] 305 if logger: 306 logger.info("Uninstalling {} {}".format(dest, require)) 307 uninstall_nbextension(dest, require, user=user, sys_prefix=sys_prefix, 308 prefix=prefix, nbextensions_dir=nbextensions_dir, logger=logger) 309 310 311def _set_nbextension_state(section, require, state, 312 user=True, sys_prefix=False, logger=None): 313 """Set whether the section's frontend should require the named nbextension 314 315 Returns True if the final state is the one requested. 316 317 Parameters 318 ---------- 319 section : string 320 The section of the server to change, one of NBCONFIG_SECTIONS 321 require : string 322 An importable AMD module inside the nbextensions static path 323 state : bool 324 The state in which to leave the extension 325 user : bool [default: True] 326 Whether to update the user's .jupyter/nbextensions directory 327 sys_prefix : bool [default: False] 328 Whether to update the sys.prefix, i.e. environment. Will override 329 `user`. 330 logger : Jupyter logger [optional] 331 Logger instance to use 332 """ 333 user = False if sys_prefix else user 334 config_dir = os.path.join( 335 _get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig') 336 cm = BaseJSONConfigManager(config_dir=config_dir) 337 if logger: 338 logger.info("{} {} extension {}...".format( 339 "Enabling" if state else "Disabling", 340 section, 341 require 342 )) 343 cm.update(section, {"load_extensions": {require: state}}) 344 345 validate_nbextension(require, logger=logger) 346 347 return cm.get(section).get(require) == state 348 349 350def _set_nbextension_state_python(state, module, user, sys_prefix, 351 logger=None): 352 """Enable or disable some nbextensions stored in a Python package 353 354 Returns a list of whether the state was achieved (i.e. changed, or was 355 already right) 356 357 Parameters 358 ---------- 359 360 state : Bool 361 Whether the extensions should be enabled 362 module : str 363 Importable Python module exposing the 364 magic-named `_jupyter_nbextension_paths` function 365 user : bool 366 Whether to enable in the user's nbextensions directory. 367 sys_prefix : bool 368 Enable/disable in the sys.prefix, i.e. environment 369 logger : Jupyter logger [optional] 370 Logger instance to use 371 """ 372 m, nbexts = _get_nbextension_metadata(module) 373 return [_set_nbextension_state(section=nbext["section"], 374 require=nbext["require"], 375 state=state, 376 user=user, sys_prefix=sys_prefix, 377 logger=logger) 378 for nbext in nbexts] 379 380 381def enable_nbextension(section, require, user=True, sys_prefix=False, 382 logger=None): 383 """Enable a named nbextension 384 385 Returns True if the final state is the one requested. 386 387 Parameters 388 ---------- 389 390 section : string 391 The section of the server to change, one of NBCONFIG_SECTIONS 392 require : string 393 An importable AMD module inside the nbextensions static path 394 user : bool [default: True] 395 Whether to enable in the user's nbextensions directory. 396 sys_prefix : bool [default: False] 397 Whether to enable in the sys.prefix, i.e. environment. Will override 398 `user` 399 logger : Jupyter logger [optional] 400 Logger instance to use 401 """ 402 return _set_nbextension_state(section=section, require=require, 403 state=True, 404 user=user, sys_prefix=sys_prefix, 405 logger=logger) 406 407 408def disable_nbextension(section, require, user=True, sys_prefix=False, 409 logger=None): 410 """Disable a named nbextension 411 412 Returns True if the final state is the one requested. 413 414 Parameters 415 ---------- 416 417 section : string 418 The section of the server to change, one of NBCONFIG_SECTIONS 419 require : string 420 An importable AMD module inside the nbextensions static path 421 user : bool [default: True] 422 Whether to enable in the user's nbextensions directory. 423 sys_prefix : bool [default: False] 424 Whether to enable in the sys.prefix, i.e. environment. Will override 425 `user`. 426 logger : Jupyter logger [optional] 427 Logger instance to use 428 """ 429 return _set_nbextension_state(section=section, require=require, 430 state=False, 431 user=user, sys_prefix=sys_prefix, 432 logger=logger) 433 434 435def _find_disable_nbextension(section, require, logger=None): 436 """Disable an nbextension from the first config location where it is enabled. 437 438 Returns True if it changed any config, False otherwise. 439 """ 440 for config_dir in jupyter_config_path(): 441 cm = BaseJSONConfigManager( 442 config_dir=os.path.join(config_dir, 'nbconfig')) 443 d = cm.get(section) 444 if d.get('load_extensions', {}).get(require, None): 445 if logger: 446 logger.info("Disabling %s extension in %s", require, config_dir) 447 cm.update(section, {'load_extensions': {require: None}}) 448 return True 449 450 return False 451 452 453def enable_nbextension_python(module, user=True, sys_prefix=False, 454 logger=None): 455 """Enable some nbextensions associated with a Python module. 456 457 Returns a list of whether the state was achieved (i.e. changed, or was 458 already right) 459 460 Parameters 461 ---------- 462 463 module : str 464 Importable Python module exposing the 465 magic-named `_jupyter_nbextension_paths` function 466 user : bool [default: True] 467 Whether to enable in the user's nbextensions directory. 468 sys_prefix : bool [default: False] 469 Whether to enable in the sys.prefix, i.e. environment. Will override 470 `user` 471 logger : Jupyter logger [optional] 472 Logger instance to use 473 """ 474 return _set_nbextension_state_python(True, module, user, sys_prefix, 475 logger=logger) 476 477 478def disable_nbextension_python(module, user=True, sys_prefix=False, 479 logger=None): 480 """Disable some nbextensions associated with a Python module. 481 482 Returns True if the final state is the one requested. 483 484 Parameters 485 ---------- 486 487 module : str 488 Importable Python module exposing the 489 magic-named `_jupyter_nbextension_paths` function 490 user : bool [default: True] 491 Whether to enable in the user's nbextensions directory. 492 sys_prefix : bool [default: False] 493 Whether to enable in the sys.prefix, i.e. environment 494 logger : Jupyter logger [optional] 495 Logger instance to use 496 """ 497 return _set_nbextension_state_python(False, module, user, sys_prefix, 498 logger=logger) 499 500 501def validate_nbextension(require, logger=None): 502 """Validate a named nbextension. 503 504 Looks across all of the nbextension directories. 505 506 Returns a list of warnings. 507 508 require : str 509 require.js path used to load the extension 510 logger : Jupyter logger [optional] 511 Logger instance to use 512 """ 513 warnings = [] 514 infos = [] 515 516 js_exists = False 517 for exts in jupyter_path('nbextensions'): 518 # Does the Javascript entrypoint actually exist on disk? 519 js = u"{}.js".format(os.path.join(exts, *require.split("/"))) 520 js_exists = os.path.exists(js) 521 if js_exists: 522 break 523 524 require_tmpl = u" - require? {} {}" 525 if js_exists: 526 infos.append(require_tmpl.format(GREEN_OK, require)) 527 else: 528 warnings.append(require_tmpl.format(RED_X, require)) 529 530 if logger: 531 if warnings: 532 logger.warning(u" - Validating: problems found:") 533 for msg in warnings: 534 logger.warning(msg) 535 for msg in infos: 536 logger.info(msg) 537 else: 538 logger.info(u" - Validating: {}".format(GREEN_OK)) 539 540 return warnings 541 542 543def validate_nbextension_python(spec, full_dest, logger=None): 544 """Assess the health of an installed nbextension 545 546 Returns a list of warnings. 547 548 Parameters 549 ---------- 550 551 spec : dict 552 A single entry of _jupyter_nbextension_paths(): 553 [{ 554 'section': 'notebook', 555 'src': 'mockextension', 556 'dest': '_mockdestination', 557 'require': '_mockdestination/index' 558 }] 559 full_dest : str 560 The on-disk location of the installed nbextension: this should end 561 with `nbextensions/<dest>` 562 logger : Jupyter logger [optional] 563 Logger instance to use 564 """ 565 infos = [] 566 warnings = [] 567 568 section = spec.get("section", None) 569 if section in NBCONFIG_SECTIONS: 570 infos.append(u" {} section: {}".format(GREEN_OK, section)) 571 else: 572 warnings.append(u" {} section: {}".format(RED_X, section)) 573 574 require = spec.get("require", None) 575 if require is not None: 576 require_path = os.path.join( 577 full_dest[0:-len(spec["dest"])], 578 u"{}.js".format(require)) 579 if os.path.exists(require_path): 580 infos.append(u" {} require: {}".format(GREEN_OK, require_path)) 581 else: 582 warnings.append(u" {} require: {}".format(RED_X, require_path)) 583 584 if logger: 585 if warnings: 586 logger.warning("- Validating: problems found:") 587 for msg in warnings: 588 logger.warning(msg) 589 for msg in infos: 590 logger.info(msg) 591 logger.warning(u"Full spec: {}".format(spec)) 592 else: 593 logger.info(u"- Validating: {}".format(GREEN_OK)) 594 595 return warnings 596 597 598#---------------------------------------------------------------------- 599# Applications 600#---------------------------------------------------------------------- 601 602from .extensions import ( 603 BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED, GREEN_OK, RED_X, 604 ArgumentConflict, _base_aliases, _base_flags, 605) 606from traitlets import Bool, Unicode 607 608flags = {} 609flags.update(_base_flags) 610flags.update({ 611 "overwrite" : ({ 612 "InstallNBExtensionApp" : { 613 "overwrite" : True, 614 }}, "Force overwrite of existing files" 615 ), 616 "symlink" : ({ 617 "InstallNBExtensionApp" : { 618 "symlink" : True, 619 }}, "Create symlink instead of copying files" 620 ), 621}) 622 623flags['s'] = flags['symlink'] 624 625aliases = {} 626aliases.update(_base_aliases) 627aliases.update({ 628 "prefix" : "InstallNBExtensionApp.prefix", 629 "nbextensions" : "InstallNBExtensionApp.nbextensions_dir", 630 "destination" : "InstallNBExtensionApp.destination", 631}) 632 633class InstallNBExtensionApp(BaseExtensionApp): 634 """Entry point for installing notebook extensions""" 635 description = """Install Jupyter notebook extensions 636 637 Usage 638 639 jupyter nbextension install path|url [--user|--sys-prefix] 640 641 This copies a file or a folder into the Jupyter nbextensions directory. 642 If a URL is given, it will be downloaded. 643 If an archive is given, it will be extracted into nbextensions. 644 If the requested files are already up to date, no action is taken 645 unless --overwrite is specified. 646 """ 647 648 examples = """ 649 jupyter nbextension install /path/to/myextension 650 """ 651 aliases = aliases 652 flags = flags 653 654 overwrite = Bool(False, config=True, help="Force overwrite of existing files") 655 symlink = Bool(False, config=True, help="Create symlinks instead of copying files") 656 657 prefix = Unicode('', config=True, help="Installation prefix") 658 nbextensions_dir = Unicode('', config=True, 659 help="Full path to nbextensions dir (probably use prefix or user)") 660 destination = Unicode('', config=True, help="Destination for the copy or symlink") 661 662 def _config_file_name_default(self): 663 """The default config file name.""" 664 return 'jupyter_notebook_config' 665 666 def install_extensions(self): 667 """Perform the installation of nbextension(s)""" 668 if len(self.extra_args)>1: 669 raise ValueError("Only one nbextension allowed at a time. " 670 "Call multiple times to install multiple extensions.") 671 672 if self.python: 673 install = install_nbextension_python 674 kwargs = {} 675 else: 676 install = install_nbextension 677 kwargs = {'destination': self.destination} 678 679 full_dests = install(self.extra_args[0], 680 overwrite=self.overwrite, 681 symlink=self.symlink, 682 user=self.user, 683 sys_prefix=self.sys_prefix, 684 prefix=self.prefix, 685 nbextensions_dir=self.nbextensions_dir, 686 logger=self.log, 687 **kwargs 688 ) 689 690 if full_dests: 691 self.log.info( 692 u"\nTo initialize this nbextension in the browser every time" 693 " the notebook (or other app) loads:\n\n" 694 " jupyter nbextension enable {}{}{}{}\n".format( 695 self.extra_args[0] if self.python else "<the entry point>", 696 " --user" if self.user else "", 697 " --py" if self.python else "", 698 " --sys-prefix" if self.sys_prefix else "" 699 ) 700 ) 701 702 def start(self): 703 """Perform the App's function as configured""" 704 if not self.extra_args: 705 sys.exit('Please specify an nbextension to install') 706 else: 707 try: 708 self.install_extensions() 709 except ArgumentConflict as e: 710 sys.exit(str(e)) 711 712 713class UninstallNBExtensionApp(BaseExtensionApp): 714 """Entry point for uninstalling notebook extensions""" 715 version = __version__ 716 description = """Uninstall Jupyter notebook extensions 717 718 Usage 719 720 jupyter nbextension uninstall path/url path/url/entrypoint 721 jupyter nbextension uninstall --py pythonPackageName 722 723 This uninstalls an nbextension. By default, it uninstalls from the 724 first directory on the search path where it finds the extension, but you can 725 uninstall from a specific location using the --user, --sys-prefix or 726 --system flags, or the --prefix option. 727 728 If you specify the --require option, the named extension will be disabled, 729 e.g.:: 730 731 jupyter nbextension uninstall myext --require myext/main 732 733 If you use the --py or --python flag, the name should be a Python module. 734 It will uninstall nbextensions listed in that module, but not the module 735 itself (which you should uninstall using a package manager such as pip). 736 """ 737 738 examples = """ 739 jupyter nbextension uninstall dest/dir dest/dir/extensionjs 740 jupyter nbextension uninstall --py extensionPyPackage 741 """ 742 743 aliases = { 744 "prefix" : "UninstallNBExtensionApp.prefix", 745 "nbextensions" : "UninstallNBExtensionApp.nbextensions_dir", 746 "require": "UninstallNBExtensionApp.require", 747 } 748 flags = BaseExtensionApp.flags.copy() 749 flags['system'] = ({'UninstallNBExtensionApp': {'system': True}}, 750 "Uninstall specifically from systemwide installation directory") 751 752 prefix = Unicode('', config=True, 753 help="Installation prefix. Overrides --user, --sys-prefix and --system" 754 ) 755 nbextensions_dir = Unicode('', config=True, 756 help="Full path to nbextensions dir (probably use prefix or user)" 757 ) 758 require = Unicode('', config=True, help="require.js module to disable loading") 759 system = Bool(False, config=True, 760 help="Uninstall specifically from systemwide installation directory" 761 ) 762 763 def _config_file_name_default(self): 764 """The default config file name.""" 765 return 'jupyter_notebook_config' 766 767 def uninstall_extension(self): 768 """Uninstall an nbextension from a specific location""" 769 kwargs = { 770 'user': self.user, 771 'sys_prefix': self.sys_prefix, 772 'prefix': self.prefix, 773 'nbextensions_dir': self.nbextensions_dir, 774 'logger': self.log 775 } 776 777 if self.python: 778 uninstall_nbextension_python(self.extra_args[0], **kwargs) 779 else: 780 if self.require: 781 kwargs['require'] = self.require 782 uninstall_nbextension(self.extra_args[0], **kwargs) 783 784 def find_uninstall_extension(self): 785 """Uninstall an nbextension from an unspecified location""" 786 name = self.extra_args[0] 787 if self.python: 788 _, nbexts = _get_nbextension_metadata(name) 789 changed = False 790 for nbext in nbexts: 791 if _find_uninstall_nbextension(nbext['dest'], logger=self.log): 792 changed = True 793 794 # Also disable it in config. 795 for section in NBCONFIG_SECTIONS: 796 _find_disable_nbextension(section, nbext['require'], 797 logger=self.log) 798 799 else: 800 changed = _find_uninstall_nbextension(name, logger=self.log) 801 802 if not changed: 803 print("No installed extension %r found." % name) 804 805 if self.require: 806 for section in NBCONFIG_SECTIONS: 807 _find_disable_nbextension(section, self.require, 808 logger=self.log) 809 810 def start(self): 811 if not self.extra_args: 812 sys.exit('Please specify an nbextension to uninstall') 813 elif len(self.extra_args) > 1: 814 sys.exit("Only one nbextension allowed at a time. " 815 "Call multiple times to uninstall multiple extensions.") 816 elif (self.user or self.sys_prefix or self.system or self.prefix 817 or self.nbextensions_dir): 818 # The user has specified a location from which to uninstall. 819 try: 820 self.uninstall_extension() 821 except ArgumentConflict as e: 822 sys.exit(str(e)) 823 else: 824 # Uninstall wherever it is. 825 self.find_uninstall_extension() 826 827 828class ToggleNBExtensionApp(BaseExtensionApp): 829 """A base class for apps that enable/disable extensions""" 830 name = "jupyter nbextension enable/disable" 831 version = __version__ 832 description = "Enable/disable an nbextension in configuration." 833 834 section = Unicode('notebook', config=True, 835 help="""Which config section to add the extension to, 'common' will affect all pages.""" 836 ) 837 user = Bool(True, config=True, help="Apply the configuration only for the current user (default)") 838 839 aliases = {'section': 'ToggleNBExtensionApp.section'} 840 841 _toggle_value = None 842 843 def _config_file_name_default(self): 844 """The default config file name.""" 845 return 'jupyter_notebook_config' 846 847 def toggle_nbextension_python(self, module): 848 """Toggle some extensions in an importable Python module. 849 850 Returns a list of booleans indicating whether the state was changed as 851 requested. 852 853 Parameters 854 ---------- 855 module : str 856 Importable Python module exposing the 857 magic-named `_jupyter_nbextension_paths` function 858 """ 859 toggle = (enable_nbextension_python if self._toggle_value 860 else disable_nbextension_python) 861 return toggle(module, 862 user=self.user, 863 sys_prefix=self.sys_prefix, 864 logger=self.log) 865 866 def toggle_nbextension(self, require): 867 """Toggle some a named nbextension by require-able AMD module. 868 869 Returns whether the state was changed as requested. 870 871 Parameters 872 ---------- 873 require : str 874 require.js path used to load the nbextension 875 """ 876 toggle = (enable_nbextension if self._toggle_value 877 else disable_nbextension) 878 return toggle(self.section, require, 879 user=self.user, sys_prefix=self.sys_prefix, 880 logger=self.log) 881 882 def start(self): 883 if not self.extra_args: 884 sys.exit('Please specify an nbextension/package to enable or disable') 885 elif len(self.extra_args) > 1: 886 sys.exit('Please specify one nbextension/package at a time') 887 if self.python: 888 self.toggle_nbextension_python(self.extra_args[0]) 889 else: 890 self.toggle_nbextension(self.extra_args[0]) 891 892 893class EnableNBExtensionApp(ToggleNBExtensionApp): 894 """An App that enables nbextensions""" 895 name = "jupyter nbextension enable" 896 description = """ 897 Enable an nbextension in frontend configuration. 898 899 Usage 900 jupyter nbextension enable [--system|--sys-prefix] 901 """ 902 _toggle_value = True 903 904 905class DisableNBExtensionApp(ToggleNBExtensionApp): 906 """An App that disables nbextensions""" 907 name = "jupyter nbextension disable" 908 description = """ 909 Disable an nbextension in frontend configuration. 910 911 Usage 912 jupyter nbextension disable [--system|--sys-prefix] 913 """ 914 _toggle_value = None 915 916 917class ListNBExtensionsApp(BaseExtensionApp): 918 """An App that lists and validates nbextensions""" 919 name = "jupyter nbextension list" 920 version = __version__ 921 description = "List all nbextensions known by the configuration system" 922 923 def list_nbextensions(self): 924 """List all the nbextensions""" 925 config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()] 926 927 print("Known nbextensions:") 928 929 for config_dir in config_dirs: 930 head = u' config dir: {}'.format(config_dir) 931 head_shown = False 932 933 cm = BaseJSONConfigManager(parent=self, config_dir=config_dir) 934 for section in NBCONFIG_SECTIONS: 935 data = cm.get(section) 936 if 'load_extensions' in data: 937 if not head_shown: 938 # only show heading if there is an nbextension here 939 print(head) 940 head_shown = True 941 print(u' {} section'.format(section)) 942 943 for require, enabled in data['load_extensions'].items(): 944 print(u' {} {}'.format( 945 require, 946 GREEN_ENABLED if enabled else RED_DISABLED)) 947 if enabled: 948 validate_nbextension(require, logger=self.log) 949 950 def start(self): 951 """Perform the App's functions as configured""" 952 self.list_nbextensions() 953 954 955_examples = """ 956jupyter nbextension list # list all configured nbextensions 957jupyter nbextension install --py <packagename> # install an nbextension from a Python package 958jupyter nbextension enable --py <packagename> # enable all nbextensions in a Python package 959jupyter nbextension disable --py <packagename> # disable all nbextensions in a Python package 960jupyter nbextension uninstall --py <packagename> # uninstall an nbextension in a Python package 961""" 962 963class NBExtensionApp(BaseExtensionApp): 964 """Base jupyter nbextension command entry point""" 965 name = "jupyter nbextension" 966 version = __version__ 967 description = "Work with Jupyter notebook extensions" 968 examples = _examples 969 970 subcommands = dict( 971 install=(InstallNBExtensionApp,"Install an nbextension"), 972 enable=(EnableNBExtensionApp, "Enable an nbextension"), 973 disable=(DisableNBExtensionApp, "Disable an nbextension"), 974 uninstall=(UninstallNBExtensionApp, "Uninstall an nbextension"), 975 list=(ListNBExtensionsApp, "List nbextensions") 976 ) 977 978 def start(self): 979 """Perform the App's functions as configured""" 980 super().start() 981 982 # The above should have called a subcommand and raised NoStart; if we 983 # get here, it didn't, so we should self.log.info a message. 984 subcmds = ", ".join(sorted(self.subcommands)) 985 sys.exit("Please supply at least one subcommand: %s" % subcmds) 986 987main = NBExtensionApp.launch_instance 988 989#------------------------------------------------------------------------------ 990# Private API 991#------------------------------------------------------------------------------ 992 993 994def _should_copy(src, dest, logger=None): 995 """Should a file be copied, if it doesn't exist, or is newer? 996 997 Returns whether the file needs to be updated. 998 999 Parameters 1000 ---------- 1001 1002 src : string 1003 A path that should exist from which to copy a file 1004 src : string 1005 A path that might exist to which to copy a file 1006 logger : Jupyter logger [optional] 1007 Logger instance to use 1008 """ 1009 if not os.path.exists(dest): 1010 return True 1011 if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6: 1012 # we add a fudge factor to work around a bug in python 2.x 1013 # that was fixed in python 3.x: https://bugs.python.org/issue12904 1014 if logger: 1015 logger.warn("Out of date: %s" % dest) 1016 return True 1017 if logger: 1018 logger.info("Up to date: %s" % dest) 1019 return False 1020 1021 1022def _maybe_copy(src, dest, logger=None): 1023 """Copy a file if it needs updating. 1024 1025 Parameters 1026 ---------- 1027 1028 src : string 1029 A path that should exist from which to copy a file 1030 src : string 1031 A path that might exist to which to copy a file 1032 logger : Jupyter logger [optional] 1033 Logger instance to use 1034 """ 1035 if _should_copy(src, dest, logger=logger): 1036 if logger: 1037 logger.info("Copying: %s -> %s" % (src, dest)) 1038 shutil.copy2(src, dest) 1039 1040 1041def _safe_is_tarfile(path): 1042 """Safe version of is_tarfile, return False on IOError. 1043 1044 Returns whether the file exists and is a tarfile. 1045 1046 Parameters 1047 ---------- 1048 1049 path : string 1050 A path that might not exist and or be a tarfile 1051 """ 1052 try: 1053 return tarfile.is_tarfile(path) 1054 except IOError: 1055 return False 1056 1057 1058def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions_dir=None): 1059 """Return the nbextension directory specified 1060 1061 Parameters 1062 ---------- 1063 1064 user : bool [default: False] 1065 Get the user's .jupyter/nbextensions directory 1066 sys_prefix : bool [default: False] 1067 Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/nbextensions 1068 prefix : str [optional] 1069 Get custom prefix 1070 nbextensions_dir : str [optional] 1071 Get what you put in 1072 """ 1073 conflicting = [ 1074 ('user', user), 1075 ('prefix', prefix), 1076 ('nbextensions_dir', nbextensions_dir), 1077 ('sys_prefix', sys_prefix), 1078 ] 1079 conflicting_set = ['{}={!r}'.format(n, v) for n, v in conflicting if v] 1080 if len(conflicting_set) > 1: 1081 raise ArgumentConflict( 1082 "cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir, but got: {}" 1083 .format(', '.join(conflicting_set))) 1084 if user: 1085 nbext = pjoin(jupyter_data_dir(), u'nbextensions') 1086 elif sys_prefix: 1087 nbext = pjoin(ENV_JUPYTER_PATH[0], u'nbextensions') 1088 elif prefix: 1089 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions') 1090 elif nbextensions_dir: 1091 nbext = nbextensions_dir 1092 else: 1093 nbext = pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions') 1094 return nbext 1095 1096 1097def _get_nbextension_metadata(module): 1098 """Get the list of nbextension paths associated with a Python module. 1099 1100 Returns a tuple of (the module, [{ 1101 'section': 'notebook', 1102 'src': 'mockextension', 1103 'dest': '_mockdestination', 1104 'require': '_mockdestination/index' 1105 }]) 1106 1107 Parameters 1108 ---------- 1109 1110 module : str 1111 Importable Python module exposing the 1112 magic-named `_jupyter_nbextension_paths` function 1113 """ 1114 m = import_item(module) 1115 if not hasattr(m, '_jupyter_nbextension_paths'): 1116 raise KeyError('The Python module {} is not a valid nbextension, ' 1117 'it is missing the `_jupyter_nbextension_paths()` method.'.format(module)) 1118 nbexts = m._jupyter_nbextension_paths() 1119 return m, nbexts 1120 1121 1122 1123if __name__ == '__main__': 1124 main() 1125