##// END OF EJS Templates
Initial take on implementing configurable destinations for nbextensions....
Jason Grout -
Show More
@@ -1,328 +1,341 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Utilities for installing Javascript extensions for the notebook"""
2 """Utilities for installing Javascript extensions for the notebook"""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import os
9 import os
10 import shutil
10 import shutil
11 import sys
11 import sys
12 import tarfile
12 import tarfile
13 import zipfile
13 import zipfile
14 import uuid
14 from os.path import basename, join as pjoin
15 from os.path import basename, join as pjoin
15
16
16 # Deferred imports
17 # Deferred imports
17 try:
18 try:
18 from urllib.parse import urlparse # Py3
19 from urllib.parse import urlparse # Py3
19 from urllib.request import urlretrieve
20 from urllib.request import urlretrieve
20 except ImportError:
21 except ImportError:
21 from urlparse import urlparse
22 from urlparse import urlparse
22 from urllib import urlretrieve
23 from urllib import urlretrieve
23
24
24 from IPython.utils.path import get_ipython_dir, ensure_dir_exists
25 from IPython.utils.path import get_ipython_dir, ensure_dir_exists
25 from IPython.utils.py3compat import string_types, cast_unicode_py2
26 from IPython.utils.py3compat import string_types, cast_unicode_py2
26 from IPython.utils.tempdir import TemporaryDirectory
27 from IPython.utils.tempdir import TemporaryDirectory
27
28
28 class ArgumentConflict(ValueError):
29 class ArgumentConflict(ValueError):
29 pass
30 pass
30
31
31 # Packagers: modify the next block if you store system-installed nbextensions elsewhere (unlikely)
32 # Packagers: modify the next block if you store system-installed nbextensions elsewhere (unlikely)
32 SYSTEM_NBEXTENSIONS_DIRS = []
33 SYSTEM_NBEXTENSIONS_DIRS = []
33
34
34 if os.name == 'nt':
35 if os.name == 'nt':
35 programdata = os.environ.get('PROGRAMDATA', None)
36 programdata = os.environ.get('PROGRAMDATA', None)
36 if programdata: # PROGRAMDATA is not defined by default on XP.
37 if programdata: # PROGRAMDATA is not defined by default on XP.
37 SYSTEM_NBEXTENSIONS_DIRS = [pjoin(programdata, 'jupyter', 'nbextensions')]
38 SYSTEM_NBEXTENSIONS_DIRS = [pjoin(programdata, 'jupyter', 'nbextensions')]
38 prefixes = []
39 prefixes = []
39 else:
40 else:
40 prefixes = [os.path.sep + pjoin('usr', 'local'), os.path.sep + 'usr']
41 prefixes = [os.path.sep + pjoin('usr', 'local'), os.path.sep + 'usr']
41
42
42 # add sys.prefix at the front
43 # add sys.prefix at the front
43 if sys.prefix not in prefixes:
44 if sys.prefix not in prefixes:
44 prefixes.insert(0, sys.prefix)
45 prefixes.insert(0, sys.prefix)
45
46
46 for prefix in prefixes:
47 for prefix in prefixes:
47 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
48 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
48 if nbext not in SYSTEM_NBEXTENSIONS_DIRS:
49 if nbext not in SYSTEM_NBEXTENSIONS_DIRS:
49 SYSTEM_NBEXTENSIONS_DIRS.append(nbext)
50 SYSTEM_NBEXTENSIONS_DIRS.append(nbext)
50
51
51 if os.name == 'nt':
52 if os.name == 'nt':
52 # PROGRAMDATA
53 # PROGRAMDATA
53 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-1]
54 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-1]
54 else:
55 else:
55 # /usr/local
56 # /usr/local
56 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-2]
57 SYSTEM_NBEXTENSIONS_INSTALL_DIR = SYSTEM_NBEXTENSIONS_DIRS[-2]
57
58
58
59
59 def _should_copy(src, dest, verbose=1):
60 def _should_copy(src, dest, verbose=1):
60 """should a file be copied?"""
61 """should a file be copied?"""
61 if not os.path.exists(dest):
62 if not os.path.exists(dest):
62 return True
63 return True
63 if os.stat(dest).st_mtime < os.stat(src).st_mtime:
64 if os.stat(dest).st_mtime < os.stat(src).st_mtime:
64 if verbose >= 2:
65 if verbose >= 2:
65 print("%s is out of date" % dest)
66 print("%s is out of date" % dest)
66 return True
67 return True
67 if verbose >= 2:
68 if verbose >= 2:
68 print("%s is up to date" % dest)
69 print("%s is up to date" % dest)
69 return False
70 return False
70
71
71
72
72 def _maybe_copy(src, dest, verbose=1):
73 def _maybe_copy(src, dest, verbose=1):
73 """copy a file if it needs updating"""
74 """copy a file if it needs updating"""
74 if _should_copy(src, dest, verbose):
75 if _should_copy(src, dest, verbose):
75 if verbose >= 1:
76 if verbose >= 1:
76 print("copying %s -> %s" % (src, dest))
77 print("copying %s -> %s" % (src, dest))
77 shutil.copy2(src, dest)
78 shutil.copy2(src, dest)
78
79
79
80
80 def _safe_is_tarfile(path):
81 def _safe_is_tarfile(path):
81 """safe version of is_tarfile, return False on IOError"""
82 """safe version of is_tarfile, return False on IOError"""
82 try:
83 try:
83 return tarfile.is_tarfile(path)
84 return tarfile.is_tarfile(path)
84 except IOError:
85 except IOError:
85 return False
86 return False
86
87
87
88
88 def check_nbextension(files, nbextensions_dir=None):
89 def check_nbextension(files, nbextensions_dir=None):
89 """Check whether nbextension files have been installed
90 """Check whether nbextension files have been installed
90
91
91 files should be a list of relative paths within nbextensions.
92 files should be a list of relative paths within nbextensions.
92
93
93 Returns True if all files are found, False if any are missing.
94 Returns True if all files are found, False if any are missing.
94 """
95 """
95 if nbextensions_dir:
96 if nbextensions_dir:
96 nbext = nbextensions_dir
97 nbext = nbextensions_dir
97 else:
98 else:
98 nbext = pjoin(get_ipython_dir(), u'nbextensions')
99 nbext = pjoin(get_ipython_dir(), u'nbextensions')
99 # make sure nbextensions dir exists
100 # make sure nbextensions dir exists
100 if not os.path.exists(nbext):
101 if not os.path.exists(nbext):
101 return False
102 return False
102
103
103 if isinstance(files, string_types):
104 if isinstance(files, string_types):
104 # one file given, turn it into a list
105 # one file given, turn it into a list
105 files = [files]
106 files = [files]
106
107
107 return all(os.path.exists(pjoin(nbext, f)) for f in files)
108 return all(os.path.exists(pjoin(nbext, f)) for f in files)
108
109
109
110
110 def install_nbextension(files, overwrite=False, symlink=False, user=False, prefix=None, nbextensions_dir=None, verbose=1):
111 def install_nbextension(files, overwrite=False, symlink=False, user=False, prefix=None, nbextensions_dir=None, verbose=1):
111 """Install a Javascript extension for the notebook
112 """Install a Javascript extension for the notebook
112
113
113 Stages files and/or directories into the nbextensions directory.
114 Stages files and/or directories into the nbextensions directory.
114 By default, this compares modification time, and only stages files that need updating.
115 By default, this compares modification time, and only stages files that need updating.
115 If `overwrite` is specified, matching files are purged before proceeding.
116 If `overwrite` is specified, matching files are purged before proceeding.
116
117
117 Parameters
118 Parameters
118 ----------
119 ----------
119
120
120 files : list(paths or URLs)
121 files : list(paths or URLs) or dict(install_name: path or URL)
121 One or more paths or URLs to existing files directories to install.
122 One or more paths or URLs to existing files directories to install.
122 These will be installed with their base name, so '/path/to/foo'
123 If given as a list, these will be installed with their base name, so '/path/to/foo'
123 will install to 'nbextensions/foo'.
124 will install to 'nbextensions/foo'. If given as a dict, such as {'bar': '/path/to/foo'},
125 then '/path/to/foo' will install to 'nbextensions/bar'.
124 Archives (zip or tarballs) will be extracted into the nbextensions directory.
126 Archives (zip or tarballs) will be extracted into the nbextensions directory.
125 overwrite : bool [default: False]
127 overwrite : bool [default: False]
126 If True, always install the files, regardless of what may already be installed.
128 If True, always install the files, regardless of what may already be installed.
127 symlink : bool [default: False]
129 symlink : bool [default: False]
128 If True, create a symlink in nbextensions, rather than copying files.
130 If True, create a symlink in nbextensions, rather than copying files.
129 Not allowed with URLs or archives. Windows support for symlinks requires
131 Not allowed with URLs or archives. Windows support for symlinks requires
130 Vista or above, Python 3, and a permission bit which only admin users
132 Vista or above, Python 3, and a permission bit which only admin users
131 have by default, so don't rely on it.
133 have by default, so don't rely on it.
132 user : bool [default: False]
134 user : bool [default: False]
133 Whether to install to the user's .ipython/nbextensions directory.
135 Whether to install to the user's .ipython/nbextensions directory.
134 Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
136 Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/nbextensions).
135 prefix : str [optional]
137 prefix : str [optional]
136 Specify install prefix, if it should differ from default (e.g. /usr/local).
138 Specify install prefix, if it should differ from default (e.g. /usr/local).
137 Will install to prefix/share/jupyter/nbextensions
139 Will install to prefix/share/jupyter/nbextensions
138 nbextensions_dir : str [optional]
140 nbextensions_dir : str [optional]
139 Specify absolute path of nbextensions directory explicitly.
141 Specify absolute path of nbextensions directory explicitly.
140 verbose : int [default: 1]
142 verbose : int [default: 1]
141 Set verbosity level. The default is 1, where file actions are printed.
143 Set verbosity level. The default is 1, where file actions are printed.
142 set verbose=2 for more output, or verbose=0 for silence.
144 set verbose=2 for more output, or verbose=0 for silence.
143 """
145 """
144 if sum(map(bool, [user, prefix, nbextensions_dir])) > 1:
146 if sum(map(bool, [user, prefix, nbextensions_dir])) > 1:
145 raise ArgumentConflict("Cannot specify more than one of user, prefix, or nbextensions_dir.")
147 raise ArgumentConflict("Cannot specify more than one of user, prefix, or nbextensions_dir.")
146 if user:
148 if user:
147 nbext = pjoin(get_ipython_dir(), u'nbextensions')
149 nbext = pjoin(get_ipython_dir(), u'nbextensions')
148 else:
150 else:
149 if prefix:
151 if prefix:
150 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
152 nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions')
151 elif nbextensions_dir:
153 elif nbextensions_dir:
152 nbext = nbextensions_dir
154 nbext = nbextensions_dir
153 else:
155 else:
154 nbext = SYSTEM_NBEXTENSIONS_INSTALL_DIR
156 nbext = SYSTEM_NBEXTENSIONS_INSTALL_DIR
155 # make sure nbextensions dir exists
157 # make sure nbextensions dir exists
156 ensure_dir_exists(nbext)
158 ensure_dir_exists(nbext)
157
159
158 if isinstance(files, string_types):
160 if isinstance(files, string_types):
159 # one file given, turn it into a list
161 # one file given, turn it into a list
160 files = [files]
162 files = [files]
163 if isinstance(files, (list,tuple)):
164 # list given, turn into dict
165 _files = {}
166 for path in map(cast_unicode_py2, files):
167 if path.startswith(('https://', 'http://')):
168 destination = urlparse(path).path.split('/')[-1]
169 elif path.endswith('.zip') or _safe_is_tarfile(path):
170 destination = str(uuid.uuid4()) # ignored for archives
171 else:
172 destination = basename(path)
173 _files[destination] = path
174 files = _files
161
175
162 for path in map(cast_unicode_py2, files):
176 for dest_basename,path in (map(cast_unicode_py2, item) for item in files.items()):
163
177
164 if path.startswith(('https://', 'http://')):
178 if path.startswith(('https://', 'http://')):
165 if symlink:
179 if symlink:
166 raise ValueError("Cannot symlink from URLs")
180 raise ValueError("Cannot symlink from URLs")
167 # Given a URL, download it
181 # Given a URL, download it
168 with TemporaryDirectory() as td:
182 with TemporaryDirectory() as td:
169 filename = urlparse(path).path.split('/')[-1]
183 filename = urlparse(path).path.split('/')[-1]
170 local_path = os.path.join(td, filename)
184 local_path = os.path.join(td, filename)
171 if verbose >= 1:
185 if verbose >= 1:
172 print("downloading %s to %s" % (path, local_path))
186 print("downloading %s to %s" % (path, local_path))
173 urlretrieve(path, local_path)
187 urlretrieve(path, local_path)
174 # now install from the local copy
188 # now install from the local copy
175 install_nbextension(local_path, overwrite=overwrite, symlink=symlink, nbextensions_dir=nbext, verbose=verbose)
189 install_nbextension({dest_basename: local_path}, overwrite=overwrite, symlink=symlink, nbextensions_dir=nbext, verbose=verbose)
176 continue
190 continue
177
191
178 # handle archives
192 # handle archives
179 archive = None
193 archive = None
180 if path.endswith('.zip'):
194 if path.endswith('.zip'):
181 archive = zipfile.ZipFile(path)
195 archive = zipfile.ZipFile(path)
182 elif _safe_is_tarfile(path):
196 elif _safe_is_tarfile(path):
183 archive = tarfile.open(path)
197 archive = tarfile.open(path)
184
198
185 if archive:
199 if archive:
186 if symlink:
200 if symlink:
187 raise ValueError("Cannot symlink from archives")
201 raise ValueError("Cannot symlink from archives")
188 if verbose >= 1:
202 if verbose >= 1:
189 print("extracting %s to %s" % (path, nbext))
203 print("extracting %s to %s" % (path, nbext))
190 archive.extractall(nbext)
204 archive.extractall(nbext)
191 archive.close()
205 archive.close()
192 continue
206 continue
193
207
194 dest = pjoin(nbext, basename(path))
208 dest = pjoin(nbext, dest_basename)
195 if overwrite and os.path.exists(dest):
209 if overwrite and os.path.exists(dest):
196 if verbose >= 1:
210 if verbose >= 1:
197 print("removing %s" % dest)
211 print("removing %s" % dest)
198 if os.path.isdir(dest) and not os.path.islink(dest):
212 if os.path.isdir(dest) and not os.path.islink(dest):
199 shutil.rmtree(dest)
213 shutil.rmtree(dest)
200 else:
214 else:
201 os.remove(dest)
215 os.remove(dest)
202
216
203 if symlink:
217 if symlink:
204 path = os.path.abspath(path)
218 path = os.path.abspath(path)
205 if not os.path.exists(dest):
219 if not os.path.exists(dest):
206 if verbose >= 1:
220 if verbose >= 1:
207 print("symlink %s -> %s" % (dest, path))
221 print("symlink %s -> %s" % (dest, path))
208 os.symlink(path, dest)
222 os.symlink(path, dest)
209 continue
223 continue
210
224
211 if os.path.isdir(path):
225 if os.path.isdir(path):
212 strip_prefix_len = len(path) - len(basename(path))
213 for parent, dirs, files in os.walk(path):
226 for parent, dirs, files in os.walk(path):
214 dest_dir = pjoin(nbext, parent[strip_prefix_len:])
227 dest_dir = pjoin(dest, parent[len(path):])
215 if not os.path.exists(dest_dir):
228 if not os.path.exists(dest_dir):
216 if verbose >= 2:
229 if verbose >= 2:
217 print("making directory %s" % dest_dir)
230 print("making directory %s" % dest_dir)
218 os.makedirs(dest_dir)
231 os.makedirs(dest_dir)
219 for file in files:
232 for file in files:
220 src = pjoin(parent, file)
233 src = pjoin(parent, file)
221 # print("%r, %r" % (dest_dir, file))
234 # print("%r, %r" % (dest_dir, file))
222 dest = pjoin(dest_dir, file)
235 dest = pjoin(dest_dir, file)
223 _maybe_copy(src, dest, verbose)
236 _maybe_copy(src, dest, verbose)
224 else:
237 else:
225 src = path
238 src = path
226 _maybe_copy(src, dest, verbose)
239 _maybe_copy(src, dest, verbose)
227
240
228 #----------------------------------------------------------------------
241 #----------------------------------------------------------------------
229 # install nbextension app
242 # install nbextension app
230 #----------------------------------------------------------------------
243 #----------------------------------------------------------------------
231
244
232 from IPython.utils.traitlets import Bool, Enum, Unicode, TraitError
245 from IPython.utils.traitlets import Bool, Enum, Unicode, TraitError
233 from IPython.core.application import BaseIPythonApplication
246 from IPython.core.application import BaseIPythonApplication
234
247
235 flags = {
248 flags = {
236 "overwrite" : ({
249 "overwrite" : ({
237 "NBExtensionApp" : {
250 "NBExtensionApp" : {
238 "overwrite" : True,
251 "overwrite" : True,
239 }}, "Force overwrite of existing files"
252 }}, "Force overwrite of existing files"
240 ),
253 ),
241 "debug" : ({
254 "debug" : ({
242 "NBExtensionApp" : {
255 "NBExtensionApp" : {
243 "verbose" : 2,
256 "verbose" : 2,
244 }}, "Extra output"
257 }}, "Extra output"
245 ),
258 ),
246 "quiet" : ({
259 "quiet" : ({
247 "NBExtensionApp" : {
260 "NBExtensionApp" : {
248 "verbose" : 0,
261 "verbose" : 0,
249 }}, "Minimal output"
262 }}, "Minimal output"
250 ),
263 ),
251 "symlink" : ({
264 "symlink" : ({
252 "NBExtensionApp" : {
265 "NBExtensionApp" : {
253 "symlink" : True,
266 "symlink" : True,
254 }}, "Create symlinks instead of copying files"
267 }}, "Create symlinks instead of copying files"
255 ),
268 ),
256 "user" : ({
269 "user" : ({
257 "NBExtensionApp" : {
270 "NBExtensionApp" : {
258 "user" : True,
271 "user" : True,
259 }}, "Install to the user's IPython directory"
272 }}, "Install to the user's IPython directory"
260 ),
273 ),
261 }
274 }
262 flags['s'] = flags['symlink']
275 flags['s'] = flags['symlink']
263
276
264 aliases = {
277 aliases = {
265 "ipython-dir" : "NBExtensionApp.ipython_dir",
278 "ipython-dir" : "NBExtensionApp.ipython_dir",
266 "prefix" : "NBExtensionApp.prefix",
279 "prefix" : "NBExtensionApp.prefix",
267 "nbextensions" : "NBExtensionApp.nbextensions_dir",
280 "nbextensions" : "NBExtensionApp.nbextensions_dir",
268 }
281 }
269
282
270 class NBExtensionApp(BaseIPythonApplication):
283 class NBExtensionApp(BaseIPythonApplication):
271 """Entry point for installing notebook extensions"""
284 """Entry point for installing notebook extensions"""
272
285
273 description = """Install IPython notebook extensions
286 description = """Install IPython notebook extensions
274
287
275 Usage
288 Usage
276
289
277 ipython install-nbextension file [more files, folders, archives or urls]
290 ipython install-nbextension file [more files, folders, archives or urls]
278
291
279 This copies files and/or folders into the IPython nbextensions directory.
292 This copies files and/or folders into the IPython nbextensions directory.
280 If a URL is given, it will be downloaded.
293 If a URL is given, it will be downloaded.
281 If an archive is given, it will be extracted into nbextensions.
294 If an archive is given, it will be extracted into nbextensions.
282 If the requested files are already up to date, no action is taken
295 If the requested files are already up to date, no action is taken
283 unless --overwrite is specified.
296 unless --overwrite is specified.
284 """
297 """
285
298
286 examples = """
299 examples = """
287 ipython install-nbextension /path/to/d3.js /path/to/myextension
300 ipython install-nbextension /path/to/d3.js /path/to/myextension
288 """
301 """
289 aliases = aliases
302 aliases = aliases
290 flags = flags
303 flags = flags
291
304
292 overwrite = Bool(False, config=True, help="Force overwrite of existing files")
305 overwrite = Bool(False, config=True, help="Force overwrite of existing files")
293 symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
306 symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
294 user = Bool(False, config=True, help="Whether to do a user install")
307 user = Bool(False, config=True, help="Whether to do a user install")
295 prefix = Unicode('', config=True, help="Installation prefix")
308 prefix = Unicode('', config=True, help="Installation prefix")
296 nbextensions_dir = Unicode('', config=True, help="Full path to nbextensions dir (probably use prefix or user)")
309 nbextensions_dir = Unicode('', config=True, help="Full path to nbextensions dir (probably use prefix or user)")
297 verbose = Enum((0,1,2), default_value=1, config=True,
310 verbose = Enum((0,1,2), default_value=1, config=True,
298 help="Verbosity level"
311 help="Verbosity level"
299 )
312 )
300
313
301 def install_extensions(self):
314 def install_extensions(self):
302 install_nbextension(self.extra_args,
315 install_nbextension(self.extra_args,
303 overwrite=self.overwrite,
316 overwrite=self.overwrite,
304 symlink=self.symlink,
317 symlink=self.symlink,
305 verbose=self.verbose,
318 verbose=self.verbose,
306 user=self.user,
319 user=self.user,
307 prefix=self.prefix,
320 prefix=self.prefix,
308 nbextensions_dir=self.nbextensions_dir,
321 nbextensions_dir=self.nbextensions_dir,
309 )
322 )
310
323
311 def start(self):
324 def start(self):
312 if not self.extra_args:
325 if not self.extra_args:
313 for nbext in [pjoin(self.ipython_dir, u'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS:
326 for nbext in [pjoin(self.ipython_dir, u'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS:
314 if os.path.exists(nbext):
327 if os.path.exists(nbext):
315 print("Notebook extensions in %s:" % nbext)
328 print("Notebook extensions in %s:" % nbext)
316 for ext in os.listdir(nbext):
329 for ext in os.listdir(nbext):
317 print(u" %s" % ext)
330 print(u" %s" % ext)
318 else:
331 else:
319 try:
332 try:
320 self.install_extensions()
333 self.install_extensions()
321 except ArgumentConflict as e:
334 except ArgumentConflict as e:
322 print(str(e), file=sys.stderr)
335 print(str(e), file=sys.stderr)
323 self.exit(1)
336 self.exit(1)
324
337
325
338
326 if __name__ == '__main__':
339 if __name__ == '__main__':
327 NBExtensionApp.launch_instance()
340 NBExtensionApp.launch_instance()
328 No newline at end of file
341
General Comments 0
You need to be logged in to leave comments. Login now