diff --git a/IPython/html/nbextensions.py b/IPython/html/nbextensions.py
index 3dcf564..6415b01 100644
--- a/IPython/html/nbextensions.py
+++ b/IPython/html/nbextensions.py
@@ -12,10 +12,21 @@ from __future__ import print_function
import os
import shutil
+import tarfile
+import zipfile
from os.path import basename, join as pjoin
+# Deferred imports
+try:
+ from urllib.parse import urlparse # Py3
+ from urllib.request import urlretrieve
+except ImportError:
+ from urlparse import urlparse
+ from urllib import urlretrieve
+
from IPython.utils.path import get_ipython_dir
from IPython.utils.py3compat import string_types, cast_unicode_py2
+from IPython.utils.tempdir import TemporaryDirectory
def _should_copy(src, dest, verbose=1):
@@ -39,20 +50,29 @@ def _maybe_copy(src, dest, verbose=1):
shutil.copy2(src, dest)
+def _safe_is_tarfile(path):
+ """safe version of is_tarfile, return False on IOError"""
+ try:
+ return tarfile.is_tarfile(path)
+ except IOError:
+ return False
+
+
def install_nbextension(files, overwrite=False, ipython_dir=None, verbose=1):
"""Install a Javascript extension for the notebook
Stages files and/or directories into IPYTHONDIR/nbextensions.
- By default, this comparse modification time, and only stages files that need updating.
+ By default, this compares modification time, and only stages files that need updating.
If `overwrite` is specified, matching files are purged before proceeding.
Parameters
----------
- files : list(paths)
- One or more paths to existing files or directories to install.
+ files : list(paths or URLs)
+ One or more paths or URLs to existing files directories to install.
These will be installed with their base name, so '/path/to/foo'
will install to 'nbextensions/foo'.
+ Archives (zip or tarballs) will be extracted into the nbextensions directory.
overwrite : bool [default: False]
If True, always install the files, regardless of what may already be installed.
ipython_dir : str [optional]
@@ -74,6 +94,33 @@ def install_nbextension(files, overwrite=False, ipython_dir=None, verbose=1):
files = [files]
for path in map(cast_unicode_py2, files):
+
+ if path.startswith(('https://', 'http://')):
+ # Given a URL, download it
+ with TemporaryDirectory() as td:
+ filename = urlparse(path).path.split('/')[-1]
+ local_path = os.path.join(td, filename)
+ if verbose >= 1:
+ print("downloading %s to %s" % (path, local_path))
+ urlretrieve(path, local_path)
+ # now install from the local copy
+ install_nbextension(local_path, overwrite, ipython_dir, verbose)
+ continue
+
+ # handle archives
+ archive = None
+ if path.endswith('.zip'):
+ archive = zipfile.ZipFile(path)
+ elif _safe_is_tarfile(path):
+ archive = tarfile.open(path)
+
+ if archive:
+ if verbose >= 1:
+ print("extracting %s to %s" % (path, nbext))
+ archive.extractall(nbext)
+ archive.close()
+ continue
+
dest = pjoin(nbext, basename(path))
if overwrite and os.path.exists(dest):
if verbose >= 1:
@@ -104,7 +151,6 @@ def install_nbextension(files, overwrite=False, ipython_dir=None, verbose=1):
# install nbextension app
#----------------------------------------------------------------------
-import logging
from IPython.utils.traitlets import Bool, Enum
from IPython.core.application import BaseIPythonApplication
@@ -136,9 +182,11 @@ class NBExtensionApp(BaseIPythonApplication):
Usage
- ipython install-nbextension file [more files or folders]
+ ipython install-nbextension file [more files, folders, archives or urls]
This copies files and/or folders into the IPython nbextensions directory.
+ If a URL is given, it will be downloaded.
+ If an archive is given, it will be extracted into nbextensions.
If the requested files are already up to date, no action is taken
unless --overwrite is specified.
"""
diff --git a/IPython/html/tests/test_nbextensions.py b/IPython/html/tests/test_nbextensions.py
index 958a8d2..57c96ff 100644
--- a/IPython/html/tests/test_nbextensions.py
+++ b/IPython/html/tests/test_nbextensions.py
@@ -14,17 +14,13 @@
import glob
import os
import re
-import time
-from contextlib import contextmanager
+import tarfile
+import zipfile
+from io import BytesIO
from os.path import basename, join as pjoin
from unittest import TestCase
-import nose.tools as nt
-
-from IPython.external.decorator import decorator
-
import IPython.testing.tools as tt
-import IPython.utils.path
from IPython.utils import py3compat
from IPython.utils.tempdir import TemporaryDirectory
from IPython.html import nbextensions
@@ -80,7 +76,8 @@ class TestInstallNBExtension(TestCase):
def assert_path_exists(self, path):
if not os.path.exists(path):
- self.fail(u"%s should exist" % path)
+ do_exist = os.listdir(os.path.dirname(path))
+ self.fail(u"%s should exist (found %s)" % (path, do_exist))
def assert_not_path_exists(self, path):
if os.path.exists(path):
@@ -199,3 +196,39 @@ class TestInstallNBExtension(TestCase):
with tt.AssertNotPrints(re.compile(r'.+')):
install_nbextension(self.src, verbose=0)
+ def test_install_zip(self):
+ path = pjoin(self.src, "myjsext.zip")
+ with zipfile.ZipFile(path, 'w') as f:
+ f.writestr("a.js", b"b();")
+ f.writestr("foo/a.js", b"foo();")
+ install_nbextension(path)
+ self.assert_installed("a.js")
+ self.assert_installed(pjoin("foo", "a.js"))
+
+ def test_install_tar(self):
+ def _add_file(f, fname, buf):
+ info = tarfile.TarInfo(fname)
+ info.size = len(buf)
+ f.addfile(info, BytesIO(buf))
+
+ for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
+ path = pjoin(self.src, "myjsext" + ext)
+ with tarfile.open(path, 'w') as f:
+ _add_file(f, "b%i.js" % i, b"b();")
+ _add_file(f, "foo/b%i.js" % i, b"foo();")
+ install_nbextension(path)
+ self.assert_installed("b%i.js" % i)
+ self.assert_installed(pjoin("foo", "b%i.js" % i))
+
+ def test_install_url(self):
+ def fake_urlretrieve(url, dest):
+ touch(dest)
+ save_urlretrieve = nbextensions.urlretrieve
+ nbextensions.urlretrieve = fake_urlretrieve
+ try:
+ install_nbextension("http://example.com/path/to/foo.js")
+ self.assert_installed("foo.js")
+ install_nbextension("https://example.com/path/to/another/bar.js")
+ self.assert_installed("bar.js")
+ finally:
+ nbextensions.urlretrieve = save_urlretrieve