##// END OF EJS Templates
Merge pull request #4778 from minrk/install-nbextensions...
Thomas Kluyver -
r15344:e4f2e7fb merge
parent child Browse files
Show More
@@ -0,0 +1,268 b''
1 # coding: utf-8
2 """Utilities for installing Javascript extensions for the notebook"""
3
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2014 The IPython Development Team
6 #
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
10
11 from __future__ import print_function
12
13 import os
14 import shutil
15 import tarfile
16 import zipfile
17 from os.path import basename, join as pjoin
18
19 # Deferred imports
20 try:
21 from urllib.parse import urlparse # Py3
22 from urllib.request import urlretrieve
23 except ImportError:
24 from urlparse import urlparse
25 from urllib import urlretrieve
26
27 from IPython.utils.path import get_ipython_dir
28 from IPython.utils.py3compat import string_types, cast_unicode_py2
29 from IPython.utils.tempdir import TemporaryDirectory
30
31
32 def _should_copy(src, dest, verbose=1):
33 """should a file be copied?"""
34 if not os.path.exists(dest):
35 return True
36 if os.stat(dest).st_mtime < os.stat(src).st_mtime:
37 if verbose >= 2:
38 print("%s is out of date" % dest)
39 return True
40 if verbose >= 2:
41 print("%s is up to date" % dest)
42 return False
43
44
45 def _maybe_copy(src, dest, verbose=1):
46 """copy a file if it needs updating"""
47 if _should_copy(src, dest, verbose):
48 if verbose >= 1:
49 print("copying %s -> %s" % (src, dest))
50 shutil.copy2(src, dest)
51
52
53 def _safe_is_tarfile(path):
54 """safe version of is_tarfile, return False on IOError"""
55 try:
56 return tarfile.is_tarfile(path)
57 except IOError:
58 return False
59
60
61 def check_nbextension(files, ipython_dir=None):
62 """Check whether nbextension files have been installed
63
64 files should be a list of relative paths within nbextensions.
65
66 Returns True if all files are found, False if any are missing.
67 """
68 ipython_dir = ipython_dir or get_ipython_dir()
69 nbext = pjoin(ipython_dir, u'nbextensions')
70 # make sure nbextensions dir exists
71 if not os.path.exists(nbext):
72 return False
73
74 if isinstance(files, string_types):
75 # one file given, turn it into a list
76 files = [files]
77
78 return all(os.path.exists(pjoin(nbext, f)) for f in files)
79
80
81 def install_nbextension(files, overwrite=False, symlink=False, ipython_dir=None, verbose=1):
82 """Install a Javascript extension for the notebook
83
84 Stages files and/or directories into IPYTHONDIR/nbextensions.
85 By default, this compares modification time, and only stages files that need updating.
86 If `overwrite` is specified, matching files are purged before proceeding.
87
88 Parameters
89 ----------
90
91 files : list(paths or URLs)
92 One or more paths or URLs to existing files directories to install.
93 These will be installed with their base name, so '/path/to/foo'
94 will install to 'nbextensions/foo'.
95 Archives (zip or tarballs) will be extracted into the nbextensions directory.
96 overwrite : bool [default: False]
97 If True, always install the files, regardless of what may already be installed.
98 symlink : bool [default: False]
99 If True, create a symlink in nbextensions, rather than copying files.
100 Not allowed with URLs or archives.
101 ipython_dir : str [optional]
102 The path to an IPython directory, if the default value is not desired.
103 get_ipython_dir() is used by default.
104 verbose : int [default: 1]
105 Set verbosity level. The default is 1, where file actions are printed.
106 set verbose=2 for more output, or verbose=0 for silence.
107 """
108
109 ipython_dir = ipython_dir or get_ipython_dir()
110 nbext = pjoin(ipython_dir, u'nbextensions')
111 # make sure nbextensions dir exists
112 if not os.path.exists(nbext):
113 os.makedirs(nbext)
114
115 if isinstance(files, string_types):
116 # one file given, turn it into a list
117 files = [files]
118
119 for path in map(cast_unicode_py2, files):
120
121 if path.startswith(('https://', 'http://')):
122 if symlink:
123 raise ValueError("Cannot symlink from URLs")
124 # Given a URL, download it
125 with TemporaryDirectory() as td:
126 filename = urlparse(path).path.split('/')[-1]
127 local_path = os.path.join(td, filename)
128 if verbose >= 1:
129 print("downloading %s to %s" % (path, local_path))
130 urlretrieve(path, local_path)
131 # now install from the local copy
132 install_nbextension(local_path, overwrite, symlink, ipython_dir, verbose)
133 continue
134
135 # handle archives
136 archive = None
137 if path.endswith('.zip'):
138 archive = zipfile.ZipFile(path)
139 elif _safe_is_tarfile(path):
140 archive = tarfile.open(path)
141
142 if archive:
143 if symlink:
144 raise ValueError("Cannot symlink from archives")
145 if verbose >= 1:
146 print("extracting %s to %s" % (path, nbext))
147 archive.extractall(nbext)
148 archive.close()
149 continue
150
151 dest = pjoin(nbext, basename(path))
152 if overwrite and os.path.exists(dest):
153 if verbose >= 1:
154 print("removing %s" % dest)
155 if os.path.isdir(dest):
156 shutil.rmtree(dest)
157 else:
158 os.remove(dest)
159
160 if symlink:
161 path = os.path.abspath(path)
162 if not os.path.exists(dest):
163 if verbose >= 1:
164 print("symlink %s -> %s" % (dest, path))
165 os.symlink(path, dest)
166 continue
167
168 if os.path.isdir(path):
169 strip_prefix_len = len(path) - len(basename(path))
170 for parent, dirs, files in os.walk(path):
171 dest_dir = pjoin(nbext, parent[strip_prefix_len:])
172 if not os.path.exists(dest_dir):
173 if verbose >= 2:
174 print("making directory %s" % dest_dir)
175 os.makedirs(dest_dir)
176 for file in files:
177 src = pjoin(parent, file)
178 # print("%r, %r" % (dest_dir, file))
179 dest = pjoin(dest_dir, file)
180 _maybe_copy(src, dest, verbose)
181 else:
182 src = path
183 _maybe_copy(src, dest, verbose)
184
185 #----------------------------------------------------------------------
186 # install nbextension app
187 #----------------------------------------------------------------------
188
189 from IPython.utils.traitlets import Bool, Enum
190 from IPython.core.application import BaseIPythonApplication
191
192 flags = {
193 "overwrite" : ({
194 "NBExtensionApp" : {
195 "overwrite" : True,
196 }}, "Force overwrite of existing files"
197 ),
198 "debug" : ({
199 "NBExtensionApp" : {
200 "verbose" : 2,
201 }}, "Extra output"
202 ),
203 "quiet" : ({
204 "NBExtensionApp" : {
205 "verbose" : 0,
206 }}, "Minimal output"
207 ),
208 "symlink" : ({
209 "NBExtensionApp" : {
210 "symlink" : True,
211 }}, "Create symlinks instead of copying files"
212 ),
213 }
214 flags['s'] = flags['symlink']
215
216 aliases = {
217 "ipython-dir" : "NBExtensionApp.ipython_dir"
218 }
219
220 class NBExtensionApp(BaseIPythonApplication):
221 """Entry point for installing notebook extensions"""
222
223 description = """Install IPython notebook extensions
224
225 Usage
226
227 ipython install-nbextension file [more files, folders, archives or urls]
228
229 This copies files and/or folders into the IPython nbextensions directory.
230 If a URL is given, it will be downloaded.
231 If an archive is given, it will be extracted into nbextensions.
232 If the requested files are already up to date, no action is taken
233 unless --overwrite is specified.
234 """
235
236 examples = """
237 ipython install-nbextension /path/to/d3.js /path/to/myextension
238 """
239 aliases = aliases
240 flags = flags
241
242 overwrite = Bool(False, config=True, help="Force overwrite of existing files")
243 symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
244 verbose = Enum((0,1,2), default_value=1, config=True,
245 help="Verbosity level"
246 )
247
248 def install_extensions(self):
249 install_nbextension(self.extra_args,
250 overwrite=self.overwrite,
251 symlink=self.symlink,
252 verbose=self.verbose,
253 ipython_dir=self.ipython_dir,
254 )
255
256 def start(self):
257 if not self.extra_args:
258 nbext = pjoin(self.ipython_dir, u'nbextensions')
259 print("Notebook extensions in %s:" % nbext)
260 for ext in os.listdir(nbext):
261 print(u" %s" % ext)
262 else:
263 self.install_extensions()
264
265
266 if __name__ == '__main__':
267 NBExtensionApp.launch_instance()
268 No newline at end of file
@@ -0,0 +1,272 b''
1 # coding: utf-8
2 """Test installation of notebook extensions"""
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2014 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13
14 import glob
15 import os
16 import re
17 import tarfile
18 import zipfile
19 from io import BytesIO
20 from os.path import basename, join as pjoin
21 from unittest import TestCase
22
23 import IPython.testing.tools as tt
24 import IPython.testing.decorators as dec
25 from IPython.utils import py3compat
26 from IPython.utils.tempdir import TemporaryDirectory
27 from IPython.html import nbextensions
28 from IPython.html.nbextensions import install_nbextension, check_nbextension
29
30 #-----------------------------------------------------------------------------
31 # Test functions
32 #-----------------------------------------------------------------------------
33
34 def touch(file, mtime=None):
35 """ensure a file exists, and set its modification time
36
37 returns the modification time of the file
38 """
39 open(file, 'a').close()
40 # set explicit mtime
41 if mtime:
42 atime = os.stat(file).st_atime
43 os.utime(file, (atime, mtime))
44 return os.stat(file).st_mtime
45
46
47 class TestInstallNBExtension(TestCase):
48
49 def tempdir(self):
50 td = TemporaryDirectory()
51 self.tempdirs.append(td)
52 return py3compat.cast_unicode(td.name)
53
54 def setUp(self):
55 self.tempdirs = []
56 src = self.src = self.tempdir()
57 self.files = files = [
58 pjoin(u'Ζ’ile'),
59 pjoin(u'βˆ‚ir', u'Ζ’ile1'),
60 pjoin(u'βˆ‚ir', u'βˆ‚ir2', u'Ζ’ile2'),
61 ]
62 for file in files:
63 fullpath = os.path.join(self.src, file)
64 parent = os.path.dirname(fullpath)
65 if not os.path.exists(parent):
66 os.makedirs(parent)
67 touch(fullpath)
68
69 self.ipdir = self.tempdir()
70 self.save_get_ipython_dir = nbextensions.get_ipython_dir
71 nbextensions.get_ipython_dir = lambda : self.ipdir
72
73 def tearDown(self):
74 for td in self.tempdirs:
75 td.cleanup()
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
77
78 def assert_path_exists(self, path):
79 if not os.path.exists(path):
80 do_exist = os.listdir(os.path.dirname(path))
81 self.fail(u"%s should exist (found %s)" % (path, do_exist))
82
83 def assert_not_path_exists(self, path):
84 if os.path.exists(path):
85 self.fail(u"%s should not exist" % path)
86
87 def assert_installed(self, relative_path, ipdir=None):
88 self.assert_path_exists(
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
90 )
91
92 def assert_not_installed(self, relative_path, ipdir=None):
93 self.assert_not_path_exists(
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
95 )
96
97 def test_create_ipython_dir(self):
98 """install_nbextension when ipython_dir doesn't exist"""
99 with TemporaryDirectory() as td:
100 ipdir = pjoin(td, u'ipython')
101 install_nbextension(self.src, ipython_dir=ipdir)
102 self.assert_path_exists(ipdir)
103 for file in self.files:
104 self.assert_installed(
105 pjoin(basename(self.src), file),
106 ipdir
107 )
108
109 def test_create_nbextensions(self):
110 with TemporaryDirectory() as ipdir:
111 install_nbextension(self.src, ipython_dir=ipdir)
112 self.assert_installed(
113 pjoin(basename(self.src), u'Ζ’ile'),
114 ipdir
115 )
116
117 def test_single_file(self):
118 file = self.files[0]
119 install_nbextension(pjoin(self.src, file))
120 self.assert_installed(file)
121
122 def test_single_dir(self):
123 d = u'βˆ‚ir'
124 install_nbextension(pjoin(self.src, d))
125 self.assert_installed(self.files[-1])
126
127 def test_install_nbextension(self):
128 install_nbextension(glob.glob(pjoin(self.src, '*')))
129 for file in self.files:
130 self.assert_installed(file)
131
132 def test_overwrite_file(self):
133 with TemporaryDirectory() as d:
134 fname = u'Ζ’.js'
135 src = pjoin(d, fname)
136 with open(src, 'w') as f:
137 f.write('first')
138 mtime = touch(src)
139 dest = pjoin(self.ipdir, u'nbextensions', fname)
140 install_nbextension(src)
141 with open(src, 'w') as f:
142 f.write('overwrite')
143 mtime = touch(src, mtime - 100)
144 install_nbextension(src, overwrite=True)
145 with open(dest) as f:
146 self.assertEqual(f.read(), 'overwrite')
147
148 def test_overwrite_dir(self):
149 with TemporaryDirectory() as src:
150 # src = py3compat.cast_unicode_py2(src)
151 base = basename(src)
152 fname = u'Ζ’.js'
153 touch(pjoin(src, fname))
154 install_nbextension(src)
155 self.assert_installed(pjoin(base, fname))
156 os.remove(pjoin(src, fname))
157 fname2 = u'βˆ‚.js'
158 touch(pjoin(src, fname2))
159 install_nbextension(src, overwrite=True)
160 self.assert_installed(pjoin(base, fname2))
161 self.assert_not_installed(pjoin(base, fname))
162
163 def test_update_file(self):
164 with TemporaryDirectory() as d:
165 fname = u'Ζ’.js'
166 src = pjoin(d, fname)
167 with open(src, 'w') as f:
168 f.write('first')
169 mtime = touch(src)
170 install_nbextension(src)
171 self.assert_installed(fname)
172 dest = pjoin(self.ipdir, u'nbextensions', fname)
173 old_mtime = os.stat(dest).st_mtime
174 with open(src, 'w') as f:
175 f.write('overwrite')
176 touch(src, mtime + 10)
177 install_nbextension(src)
178 with open(dest) as f:
179 self.assertEqual(f.read(), 'overwrite')
180
181 def test_skip_old_file(self):
182 with TemporaryDirectory() as d:
183 fname = u'Ζ’.js'
184 src = pjoin(d, fname)
185 mtime = touch(src)
186 install_nbextension(src)
187 self.assert_installed(fname)
188 dest = pjoin(self.ipdir, u'nbextensions', fname)
189 old_mtime = os.stat(dest).st_mtime
190
191 mtime = touch(src, mtime - 100)
192 install_nbextension(src)
193 new_mtime = os.stat(dest).st_mtime
194 self.assertEqual(new_mtime, old_mtime)
195
196 def test_quiet(self):
197 with tt.AssertNotPrints(re.compile(r'.+')):
198 install_nbextension(self.src, verbose=0)
199
200 def test_install_zip(self):
201 path = pjoin(self.src, "myjsext.zip")
202 with zipfile.ZipFile(path, 'w') as f:
203 f.writestr("a.js", b"b();")
204 f.writestr("foo/a.js", b"foo();")
205 install_nbextension(path)
206 self.assert_installed("a.js")
207 self.assert_installed(pjoin("foo", "a.js"))
208
209 def test_install_tar(self):
210 def _add_file(f, fname, buf):
211 info = tarfile.TarInfo(fname)
212 info.size = len(buf)
213 f.addfile(info, BytesIO(buf))
214
215 for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
216 path = pjoin(self.src, "myjsext" + ext)
217 with tarfile.open(path, 'w') as f:
218 _add_file(f, "b%i.js" % i, b"b();")
219 _add_file(f, "foo/b%i.js" % i, b"foo();")
220 install_nbextension(path)
221 self.assert_installed("b%i.js" % i)
222 self.assert_installed(pjoin("foo", "b%i.js" % i))
223
224 def test_install_url(self):
225 def fake_urlretrieve(url, dest):
226 touch(dest)
227 save_urlretrieve = nbextensions.urlretrieve
228 nbextensions.urlretrieve = fake_urlretrieve
229 try:
230 install_nbextension("http://example.com/path/to/foo.js")
231 self.assert_installed("foo.js")
232 install_nbextension("https://example.com/path/to/another/bar.js")
233 self.assert_installed("bar.js")
234 finally:
235 nbextensions.urlretrieve = save_urlretrieve
236
237 def test_check_nbextension(self):
238 with TemporaryDirectory() as d:
239 f = u'Ζ’.js'
240 src = pjoin(d, f)
241 touch(src)
242 install_nbextension(src)
243
244 assert check_nbextension(f, self.ipdir)
245 assert check_nbextension([f], self.ipdir)
246 assert not check_nbextension([f, pjoin('dne', f)], self.ipdir)
247
248 @dec.skip_win32
249 def test_install_symlink(self):
250 with TemporaryDirectory() as d:
251 f = u'Ζ’.js'
252 src = pjoin(d, f)
253 touch(src)
254 install_nbextension(src, symlink=True)
255 dest = pjoin(self.ipdir, u'nbextensions', f)
256 assert os.path.islink(dest)
257 link = os.readlink(dest)
258 self.assertEqual(link, src)
259
260 def test_install_symlink_bad(self):
261 with self.assertRaises(ValueError):
262 install_nbextension("http://example.com/foo.js", symlink=True)
263
264 with TemporaryDirectory() as d:
265 zf = u'Ζ’.zip'
266 zsrc = pjoin(d, zf)
267 with zipfile.ZipFile(zsrc, 'w') as z:
268 z.writestr("a.js", b"b();")
269
270 with self.assertRaises(ValueError):
271 install_nbextension(zsrc, symlink=True)
272
@@ -5,3 +5,5 b' import os'
5 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
5 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
6
6
7 del os
7 del os
8
9 from .nbextensions import install_nbextension No newline at end of file
@@ -12,6 +12,34 b" IPython.namespace('IPython.utils');"
12
12
13 IPython.utils = (function (IPython) {
13 IPython.utils = (function (IPython) {
14 "use strict";
14 "use strict";
15
16 IPython.load_extensions = function () {
17 // load one or more IPython notebook extensions with requirejs
18
19 var extensions = [];
20 var extension_names = arguments;
21 for (var i = 0; i < extension_names.length; i++) {
22 extensions.push("nbextensions/" + arguments[i]);
23 }
24
25 require(extensions,
26 function () {
27 for (var i = 0; i < arguments.length; i++) {
28 var ext = arguments[i];
29 var ext_name = extension_names[i];
30 // success callback
31 console.log("Loaded extension: " + ext_name);
32 if (ext && ext.load_ipython_extension !== undefined) {
33 ext.load_ipython_extension();
34 }
35 }
36 },
37 function (err) {
38 // failure callback
39 console.log("Failed to load extension(s):", err.requireModules, err);
40 }
41 );
42 };
15
43
16 //============================================================================
44 //============================================================================
17 // Cross-browser RegEx Split
45 // Cross-browser RegEx Split
@@ -224,7 +224,7 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):'
224 StoreMagics,
224 StoreMagics,
225 ]
225 ]
226
226
227 subcommands = Dict(dict(
227 subcommands = dict(
228 qtconsole=('IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp',
228 qtconsole=('IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp',
229 """Launch the IPython Qt Console."""
229 """Launch the IPython Qt Console."""
230 ),
230 ),
@@ -252,7 +252,11 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):'
252 trust=('IPython.nbformat.sign.TrustNotebookApp',
252 trust=('IPython.nbformat.sign.TrustNotebookApp',
253 "Sign notebooks to trust their potentially unsafe contents at load."
253 "Sign notebooks to trust their potentially unsafe contents at load."
254 ),
254 ),
255 ))
255 )
256 subcommands['install-nbextension'] = (
257 "IPython.html.nbextensions.NBExtensionApp",
258 "Install IPython notebook extension files"
259 )
256
260
257 # *do* autocreate requested profile, but don't create the config file.
261 # *do* autocreate requested profile, but don't create the config file.
258 auto_create=Bool(True)
262 auto_create=Bool(True)
@@ -327,6 +327,8 b' else:'
327 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
327 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
328 super(MyStringIO, self).write(s)
328 super(MyStringIO, self).write(s)
329
329
330 _re_type = type(re.compile(r''))
331
330 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
332 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
331 -------
333 -------
332 {2!s}
334 {2!s}
@@ -347,7 +349,7 b' class AssertPrints(object):'
347 """
349 """
348 def __init__(self, s, channel='stdout', suppress=True):
350 def __init__(self, s, channel='stdout', suppress=True):
349 self.s = s
351 self.s = s
350 if isinstance(self.s, py3compat.string_types):
352 if isinstance(self.s, (py3compat.string_types, _re_type)):
351 self.s = [self.s]
353 self.s = [self.s]
352 self.channel = channel
354 self.channel = channel
353 self.suppress = suppress
355 self.suppress = suppress
@@ -366,7 +368,10 b' class AssertPrints(object):'
366 setattr(sys, self.channel, self.orig_stream)
368 setattr(sys, self.channel, self.orig_stream)
367 printed = self.buffer.getvalue()
369 printed = self.buffer.getvalue()
368 for s in self.s:
370 for s in self.s:
369 assert s in printed, notprinted_msg.format(s, self.channel, printed)
371 if isinstance(s, _re_type):
372 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
373 else:
374 assert s in printed, notprinted_msg.format(s, self.channel, printed)
370 return False
375 return False
371
376
372 printed_msg = """Found {0!r} in printed output (on {1}):
377 printed_msg = """Found {0!r} in printed output (on {1}):
@@ -387,7 +392,10 b' class AssertNotPrints(AssertPrints):'
387 setattr(sys, self.channel, self.orig_stream)
392 setattr(sys, self.channel, self.orig_stream)
388 printed = self.buffer.getvalue()
393 printed = self.buffer.getvalue()
389 for s in self.s:
394 for s in self.s:
390 assert s not in printed, printed_msg.format(s, self.channel, printed)
395 if isinstance(s, _re_type):
396 assert not s.search(printed), printed_msg.format(s.pattern, self.channel, printed)
397 else:
398 assert s not in printed, printed_msg.format(s, self.channel, printed)
391 return False
399 return False
392
400
393 @contextmanager
401 @contextmanager
General Comments 0
You need to be logged in to leave comments. Login now