diff --git a/IPython/html/nbextensions.py b/IPython/html/nbextensions.py index d254b76..df4f040 100644 --- a/IPython/html/nbextensions.py +++ b/IPython/html/nbextensions.py @@ -8,6 +8,7 @@ from __future__ import print_function import os import shutil +import sys import tarfile import zipfile from os.path import basename, join as pjoin @@ -25,6 +26,34 @@ from IPython.utils.py3compat import string_types, cast_unicode_py2 from IPython.utils.tempdir import TemporaryDirectory +# Packagers: modify the next block if you store system-installed nbextensions elsewhere (unlikely) +SYSTEM_NBEXTENSIONS_DIRS = [] + +if os.name == 'nt': + programdata = os.environ.get('PROGRAMDATA', None) + if programdata: # PROGRAMDATA is not defined by default on XP. + SYSTEM_NBEXTENSIONS_DIRS = [pjoin(programdata, 'jupyter', 'nbextensions')] + prefixes = [] +else: + prefixes = ['/usr/local', '/usr'] + +# add sys.prefix at the front +if sys.prefix not in prefixes: + prefixes.insert(0, sys.prefix) + +for prefix in prefixes: + nbext = os.path.join(prefix, 'share', 'jupyter', 'nbextensions') + if nbext not in SYSTEM_NBEXTENSIONS_DIRS: + SYSTEM_NBEXTENSIONS_DIRS.append(nbext) + +if os.name == 'nt': + # PROGRAMDATA + SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-1] +else: + # /usr/local + SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-2] + + def _should_copy(src, dest, verbose=1): """should a file be copied?""" if not os.path.exists(dest): @@ -54,15 +83,17 @@ def _safe_is_tarfile(path): return False -def check_nbextension(files, ipython_dir=None): +def check_nbextension(files, nbextensions=None): """Check whether nbextension files have been installed files should be a list of relative paths within nbextensions. Returns True if all files are found, False if any are missing. """ - ipython_dir = ipython_dir or get_ipython_dir() - nbext = pjoin(ipython_dir, u'nbextensions') + if nbextensions: + nbext = nbextensions + else: + nbext = pjoin(get_ipython_dir(), u'nbextensions') # make sure nbextensions dir exists if not os.path.exists(nbext): return False @@ -74,7 +105,7 @@ def check_nbextension(files, ipython_dir=None): return all(os.path.exists(pjoin(nbext, f)) for f in files) -def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, verbose=1): +def install_nbextension(files, overwrite=False, symlink=False, user=False, prefix=None, nbextensions=None, verbose=1): """Install a Javascript extension for the notebook Stages files and/or directories into IPYTHONDIR/nbextensions. @@ -96,16 +127,29 @@ def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, Not allowed with URLs or archives. Windows support for symlinks requires Vista or above, Python 3, and a permission bit which only admin users have by default, so don't rely on it. - ipython_dir : str [optional] - The path to an IPython directory, if the default value is not desired. - get_ipython_dir() is used by default. + user : bool [default: False] + Whether to install to the user's .ipython/nbextensions directory. + Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions). + prefix : str [optional] + Specify install prefix, if it should differ from default (e.g. /usr/local). + Will install to prefix/share/jupyter/nbextensions + nbextensions : str [optional] + Specify absolute path of nbextensions directory explicitly. verbose : int [default: 1] Set verbosity level. The default is 1, where file actions are printed. set verbose=2 for more output, or verbose=0 for silence. """ - - ipython_dir = ipython_dir or get_ipython_dir() - nbext = pjoin(ipython_dir, u'nbextensions') + if sum(map(bool, [user, prefix, nbextensions])) > 1: + raise ValueError("Cannot specify more than one of user, prefix, or nbextensions.") + if user: + nbext = pjoin(get_ipython_dir(), u'nbextensions') + else: + if prefix: + nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions') + elif nbextensions: + nbext = nbextensions + else: + nbext = SYSTEM_NBEXTENSIONS_INSTALL_DIR # make sure nbextensions dir exists ensure_dir_exists(nbext) @@ -126,7 +170,7 @@ def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, print("downloading %s to %s" % (path, local_path)) urlretrieve(path, local_path) # now install from the local copy - install_nbextension(local_path, overwrite, symlink, ipython_dir, verbose) + install_nbextension(local_path, overwrite=overwrite, symlink=symlink, nbextensions=nbext, verbose=verbose) continue # handle archives @@ -183,7 +227,7 @@ def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, # install nbextension app #---------------------------------------------------------------------- -from IPython.utils.traitlets import Bool, Enum +from IPython.utils.traitlets import Bool, Enum, Unicode, TraitError from IPython.core.application import BaseIPythonApplication flags = { @@ -207,11 +251,18 @@ flags = { "symlink" : True, }}, "Create symlinks instead of copying files" ), + "user" : ({ + "NBExtensionApp" : { + "user" : True, + }}, "Install to the user's IPython directory" + ), } flags['s'] = flags['symlink'] aliases = { - "ipython-dir" : "NBExtensionApp.ipython_dir" + "ipython-dir" : "NBExtensionApp.ipython_dir", + "prefix" : "NBExtensionApp.prefix", + "nbextensions" : "NBExtensionApp.nbextensions", } class NBExtensionApp(BaseIPythonApplication): @@ -238,25 +289,36 @@ class NBExtensionApp(BaseIPythonApplication): overwrite = Bool(False, config=True, help="Force overwrite of existing files") symlink = Bool(False, config=True, help="Create symlinks instead of copying files") + user = Bool(False, config=True, help="Whether to do a user install") + prefix = Unicode('', config=True, help="Installation prefix") + nbextensions = Unicode('', config=True, help="Full path to nbextensions (probably use prefix or user)") verbose = Enum((0,1,2), default_value=1, config=True, help="Verbosity level" ) + def check_install(): + if sum(map(bool, [user, prefix, nbextensions])) > 1: + raise TraitError("Cannot specify more than one of user, prefix, or nbextensions.") + def install_extensions(self): install_nbextension(self.extra_args, overwrite=self.overwrite, symlink=self.symlink, verbose=self.verbose, - ipython_dir=self.ipython_dir, + user=self.user, + prefix=self.prefix, + nbextensions=self.nbextensions, ) def start(self): if not self.extra_args: - nbext = pjoin(self.ipython_dir, u'nbextensions') - print("Notebook extensions in %s:" % nbext) - for ext in os.listdir(nbext): - print(u" %s" % ext) + for nbext in [pjoin(self.ipython_dir, u'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS: + if os.path.exists(nbext): + print("Notebook extensions in %s:" % nbext) + for ext in os.listdir(nbext): + print(u" %s" % ext) else: + self.check_install() self.install_extensions() diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 71883e6..38bbc20 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -90,6 +90,7 @@ from IPython.utils import py3compat from IPython.utils.path import filefind, get_ipython_dir from IPython.utils.sysinfo import get_sys_info +from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS from .utils import url_path_join #----------------------------------------------------------------------------- @@ -566,11 +567,14 @@ class NotebookApp(BaseIPythonApplication): """return extra paths + the default locations""" return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST - nbextensions_path = List(Unicode, config=True, - help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions""" + extra_nbextensions_path = List(Unicode, config=True, + help="""extra paths to look for Javascript notebook extensions""" ) - def _nbextensions_path_default(self): - return [os.path.join(get_ipython_dir(), 'nbextensions')] + + @property + def nbextensions_path(self): + """The path to look for Javascript notebook extensions""" + return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS websocket_url = Unicode("", config=True, help="""The base URL for websockets, diff --git a/IPython/html/tests/test_nbextensions.py b/IPython/html/tests/test_nbextensions.py index f400c8a..1f0351f 100644 --- a/IPython/html/tests/test_nbextensions.py +++ b/IPython/html/tests/test_nbextensions.py @@ -1,15 +1,8 @@ # coding: utf-8 """Test installation of notebook extensions""" -#----------------------------------------------------------------------------- -# Copyright (C) 2014 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import glob import os @@ -27,9 +20,6 @@ from IPython.utils.tempdir import TemporaryDirectory from IPython.html import nbextensions from IPython.html.nbextensions import install_nbextension, check_nbextension -#----------------------------------------------------------------------------- -# Test functions -#----------------------------------------------------------------------------- def touch(file, mtime=None): """ensure a file exists, and set its modification time @@ -43,7 +33,6 @@ def touch(file, mtime=None): os.utime(file, (atime, mtime)) return os.stat(file).st_mtime - class TestInstallNBExtension(TestCase): def tempdir(self): @@ -69,12 +58,15 @@ class TestInstallNBExtension(TestCase): self.ipdir = self.tempdir() self.save_get_ipython_dir = nbextensions.get_ipython_dir nbextensions.get_ipython_dir = lambda : self.ipdir + self.save_system_dir = nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR + nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = self.tempdir() def tearDown(self): + nbextensions.get_ipython_dir = self.save_get_ipython_dir + nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.save_system_dir for td in self.tempdirs: td.cleanup() - nbextensions.get_ipython_dir = self.save_get_ipython_dir - + def assert_dir_exists(self, path): if not os.path.exists(path): do_exist = os.listdir(os.path.dirname(path)) @@ -84,21 +76,29 @@ class TestInstallNBExtension(TestCase): if os.path.exists(path): self.fail(u"%s should not exist" % path) - def assert_installed(self, relative_path, ipdir=None): + def assert_installed(self, relative_path, user=False): + if user: + nbext = pjoin(self.ipdir, u'nbextensions') + else: + nbext = self.system_nbext self.assert_dir_exists( - pjoin(ipdir or self.ipdir, u'nbextensions', relative_path) + pjoin(nbext, relative_path) ) - def assert_not_installed(self, relative_path, ipdir=None): + def assert_not_installed(self, relative_path, user=False): + if user: + nbext = pjoin(self.ipdir, u'nbextensions') + else: + nbext = self.system_nbext self.assert_not_dir_exists( - pjoin(ipdir or self.ipdir, u'nbextensions', relative_path) + pjoin(nbext, relative_path) ) def test_create_ipython_dir(self): """install_nbextension when ipython_dir doesn't exist""" with TemporaryDirectory() as td: - ipdir = pjoin(td, u'ipython') - install_nbextension(self.src, ipython_dir=ipdir) + self.ipdir = ipdir = pjoin(td, u'ipython') + install_nbextension(self.src, user=True) self.assert_dir_exists(ipdir) for file in self.files: self.assert_installed( @@ -106,12 +106,22 @@ class TestInstallNBExtension(TestCase): ipdir ) - def test_create_nbextensions(self): - with TemporaryDirectory() as ipdir: - install_nbextension(self.src, ipython_dir=ipdir) + def test_create_nbextensions_user(self): + with TemporaryDirectory() as td: + self.ipdir = ipdir = pjoin(td, u'ipython') + install_nbextension(self.src, user=True) + self.assert_installed( + pjoin(basename(self.src), u'ƒile'), + user=True + ) + + def test_create_nbextensions_system(self): + with TemporaryDirectory() as td: + nbextensions.SYSTEM_NBEXTENSIONS_INSTALL_DIR = self.system_nbext = pjoin(td, u'nbextensions') + install_nbextension(self.src, user=False) self.assert_installed( pjoin(basename(self.src), u'ƒile'), - ipdir + user=False ) def test_single_file(self): @@ -136,7 +146,7 @@ class TestInstallNBExtension(TestCase): with open(src, 'w') as f: f.write('first') mtime = touch(src) - dest = pjoin(self.ipdir, u'nbextensions', fname) + dest = pjoin(self.system_nbext, fname) install_nbextension(src) with open(src, 'w') as f: f.write('overwrite') @@ -147,7 +157,6 @@ class TestInstallNBExtension(TestCase): def test_overwrite_dir(self): with TemporaryDirectory() as src: - # src = py3compat.cast_unicode_py2(src) base = basename(src) fname = u'ƒ.js' touch(pjoin(src, fname)) @@ -169,7 +178,7 @@ class TestInstallNBExtension(TestCase): mtime = touch(src) install_nbextension(src) self.assert_installed(fname) - dest = pjoin(self.ipdir, u'nbextensions', fname) + dest = pjoin(self.system_nbext, fname) old_mtime = os.stat(dest).st_mtime with open(src, 'w') as f: f.write('overwrite') @@ -185,7 +194,7 @@ class TestInstallNBExtension(TestCase): mtime = touch(src) install_nbextension(src) self.assert_installed(fname) - dest = pjoin(self.ipdir, u'nbextensions', fname) + dest = pjoin(self.system_nbext, fname) old_mtime = os.stat(dest).st_mtime mtime = touch(src, mtime - 100) @@ -239,11 +248,12 @@ class TestInstallNBExtension(TestCase): f = u'ƒ.js' src = pjoin(d, f) touch(src) - install_nbextension(src) + install_nbextension(src, user=True) - assert check_nbextension(f, self.ipdir) - assert check_nbextension([f], self.ipdir) - assert not check_nbextension([f, pjoin('dne', f)], self.ipdir) + nbext = pjoin(self.ipdir, u'nbextensions') + assert check_nbextension(f, nbext) + assert check_nbextension([f], nbext) + assert not check_nbextension([f, pjoin('dne', f)], nbext) @dec.skip_win32 def test_install_symlink(self): @@ -252,7 +262,7 @@ class TestInstallNBExtension(TestCase): src = pjoin(d, f) touch(src) install_nbextension(src, symlink=True) - dest = pjoin(self.ipdir, u'nbextensions', f) + dest = pjoin(self.system_nbext, f) assert os.path.islink(dest) link = os.readlink(dest) self.assertEqual(link, src)