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