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