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