diff --git a/IPython/html/nbextensions.py b/IPython/html/nbextensions.py new file mode 100644 index 0000000..78b3eba --- /dev/null +++ b/IPython/html/nbextensions.py @@ -0,0 +1,101 @@ +# coding: utf-8 +"""Utilities for installing Javascript extensions for the notebook""" + +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +from __future__ import print_function + +import os +import shutil +from os.path import basename, join as pjoin + +from IPython.utils.path import get_ipython_dir +from IPython.utils.py3compat import string_types, cast_unicode_py2 + + +def _should_copy(src, dest, verbose=1): + """should a file be copied?""" + if not os.path.exists(dest): + return True + if os.stat(dest).st_mtime < os.stat(src).st_mtime: + if verbose >= 2: + print("%s is out of date" % dest) + return True + if verbose >= 2: + print("%s is up to date" % dest) + return False + + +def _maybe_copy(src, dest, verbose=1): + """copy a file if it needs updating""" + if _should_copy(src, dest, verbose): + if verbose >= 1: + print("copying %s -> %s" % (src, dest)) + shutil.copy2(src, dest) + + +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. + 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. + These will be installed with their base name, so '/path/to/foo' + will install to 'nbextensions/foo'. + overwrite : bool [default: False] + If True, always install the files, regardless of what may already be installed. + ipython_dir : str [optional] + The path to an IPython directory, if the default value is not desired. + get_ipython_dir() is used by default. + 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') + # make sure nbextensions dir exists + if not os.path.exists(nbext): + os.makedirs(nbext) + + if isinstance(files, string_types): + # one file given, turn it into a list + files = [files] + + for path in map(cast_unicode_py2, files): + dest = pjoin(nbext, basename(path)) + if overwrite and os.path.exists(dest): + if verbose >= 1: + print("removing %s" % dest) + if os.path.isdir(dest): + shutil.rmtree(dest) + else: + os.remove(dest) + + if os.path.isdir(path): + strip_prefix_len = len(path) - len(basename(path)) + for parent, dirs, files in os.walk(path): + dest_dir = pjoin(nbext, parent[strip_prefix_len:]) + if not os.path.exists(dest_dir): + if verbose >= 2: + print("making directory %s" % dest_dir) + os.makedirs(dest_dir) + for file in files: + src = pjoin(parent, file) + # print("%r, %r" % (dest_dir, file)) + dest = pjoin(dest_dir, file) + _maybe_copy(src, dest, verbose) + else: + src = path + _maybe_copy(src, dest, verbose) diff --git a/IPython/html/tests/test_nbextensions.py b/IPython/html/tests/test_nbextensions.py new file mode 100644 index 0000000..958a8d2 --- /dev/null +++ b/IPython/html/tests/test_nbextensions.py @@ -0,0 +1,201 @@ +# 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 +#----------------------------------------------------------------------------- + +import glob +import os +import re +import time +from contextlib import contextmanager +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 +from IPython.html.nbextensions import install_nbextension + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +def touch(file, mtime=None): + """ensure a file exists, and set its modification time + + returns the modification time of the file + """ + open(file, 'a').close() + # set explicit mtime + if mtime: + atime = os.stat(file).st_atime + os.utime(file, (atime, mtime)) + return os.stat(file).st_mtime + + +class TestInstallNBExtension(TestCase): + + def tempdir(self): + td = TemporaryDirectory() + self.tempdirs.append(td) + return py3compat.cast_unicode(td.name) + + def setUp(self): + self.tempdirs = [] + src = self.src = self.tempdir() + self.files = files = [ + pjoin(u'ƒile'), + pjoin(u'∂ir', u'ƒile1'), + pjoin(u'∂ir', u'∂ir2', u'ƒile2'), + ] + for file in files: + fullpath = os.path.join(self.src, file) + parent = os.path.dirname(fullpath) + if not os.path.exists(parent): + os.makedirs(parent) + touch(fullpath) + + self.ipdir = self.tempdir() + self.save_get_ipython_dir = nbextensions.get_ipython_dir + nbextensions.get_ipython_dir = lambda : self.ipdir + + def tearDown(self): + for td in self.tempdirs: + td.cleanup() + nbextensions.get_ipython_dir = self.save_get_ipython_dir + + def assert_path_exists(self, path): + if not os.path.exists(path): + self.fail(u"%s should exist" % path) + + def assert_not_path_exists(self, path): + if os.path.exists(path): + self.fail(u"%s should not exist" % path) + + def assert_installed(self, relative_path, ipdir=None): + self.assert_path_exists( + pjoin(ipdir or self.ipdir, u'nbextensions', relative_path) + ) + + def assert_not_installed(self, relative_path, ipdir=None): + self.assert_not_path_exists( + pjoin(ipdir or self.ipdir, u'nbextensions', 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.assert_path_exists(ipdir) + for file in self.files: + self.assert_installed( + pjoin(basename(self.src), file), + ipdir + ) + + def test_create_nbextensions(self): + with TemporaryDirectory() as ipdir: + install_nbextension(self.src, ipython_dir=ipdir) + self.assert_installed( + pjoin(basename(self.src), u'ƒile'), + ipdir + ) + + def test_single_file(self): + file = self.files[0] + install_nbextension(pjoin(self.src, file)) + self.assert_installed(file) + + def test_single_dir(self): + d = u'∂ir' + install_nbextension(pjoin(self.src, d)) + self.assert_installed(self.files[-1]) + + def test_install_nbextension(self): + install_nbextension(glob.glob(pjoin(self.src, '*'))) + for file in self.files: + self.assert_installed(file) + + def test_overwrite_file(self): + with TemporaryDirectory() as d: + fname = u'ƒ.js' + src = pjoin(d, fname) + with open(src, 'w') as f: + f.write('first') + mtime = touch(src) + dest = pjoin(self.ipdir, u'nbextensions', fname) + install_nbextension(src) + with open(src, 'w') as f: + f.write('overwrite') + mtime = touch(src, mtime - 100) + install_nbextension(src, overwrite=True) + with open(dest) as f: + self.assertEqual(f.read(), 'overwrite') + + 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)) + install_nbextension(src) + self.assert_installed(pjoin(base, fname)) + os.remove(pjoin(src, fname)) + fname2 = u'∂.js' + touch(pjoin(src, fname2)) + install_nbextension(src, overwrite=True) + self.assert_installed(pjoin(base, fname2)) + self.assert_not_installed(pjoin(base, fname)) + + def test_update_file(self): + with TemporaryDirectory() as d: + fname = u'ƒ.js' + src = pjoin(d, fname) + with open(src, 'w') as f: + f.write('first') + mtime = touch(src) + install_nbextension(src) + self.assert_installed(fname) + dest = pjoin(self.ipdir, u'nbextensions', fname) + old_mtime = os.stat(dest).st_mtime + with open(src, 'w') as f: + f.write('overwrite') + touch(src, mtime + 10) + install_nbextension(src) + with open(dest) as f: + self.assertEqual(f.read(), 'overwrite') + + def test_skip_old_file(self): + with TemporaryDirectory() as d: + fname = u'ƒ.js' + src = pjoin(d, fname) + mtime = touch(src) + install_nbextension(src) + self.assert_installed(fname) + dest = pjoin(self.ipdir, u'nbextensions', fname) + old_mtime = os.stat(dest).st_mtime + + mtime = touch(src, mtime - 100) + install_nbextension(src) + new_mtime = os.stat(dest).st_mtime + self.assertEqual(new_mtime, old_mtime) + + def test_quiet(self): + with tt.AssertNotPrints(re.compile(r'.+')): + install_nbextension(self.src, verbose=0) +