##// 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
@@ -1,7 +1,9 b''
1 1 """The IPython HTML Notebook"""
2 2
3 3 import os
4 4 # Packagers: modify this line if you store the notebook static files elsewhere
5 5 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
6 6
7 7 del os
8
9 from .nbextensions import install_nbextension No newline at end of file
@@ -1,547 +1,575 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Utilities
10 10 //============================================================================
11 11 IPython.namespace('IPython.utils');
12 12
13 13 IPython.utils = (function (IPython) {
14 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 45 // Cross-browser RegEx Split
18 46 //============================================================================
19 47
20 48 // This code has been MODIFIED from the code licensed below to not replace the
21 49 // default browser split. The license is reproduced here.
22 50
23 51 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
24 52 /*!
25 53 * Cross-Browser Split 1.1.1
26 54 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
27 55 * Available under the MIT License
28 56 * ECMAScript compliant, uniform cross-browser split method
29 57 */
30 58
31 59 /**
32 60 * Splits a string into an array of strings using a regex or string
33 61 * separator. Matches of the separator are not included in the result array.
34 62 * However, if `separator` is a regex that contains capturing groups,
35 63 * backreferences are spliced into the result each time `separator` is
36 64 * matched. Fixes browser bugs compared to the native
37 65 * `String.prototype.split` and can be used reliably cross-browser.
38 66 * @param {String} str String to split.
39 67 * @param {RegExp|String} separator Regex or string to use for separating
40 68 * the string.
41 69 * @param {Number} [limit] Maximum number of items to include in the result
42 70 * array.
43 71 * @returns {Array} Array of substrings.
44 72 * @example
45 73 *
46 74 * // Basic use
47 75 * regex_split('a b c d', ' ');
48 76 * // -> ['a', 'b', 'c', 'd']
49 77 *
50 78 * // With limit
51 79 * regex_split('a b c d', ' ', 2);
52 80 * // -> ['a', 'b']
53 81 *
54 82 * // Backreferences in result array
55 83 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
56 84 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
57 85 */
58 86 var regex_split = function (str, separator, limit) {
59 87 // If `separator` is not a regex, use `split`
60 88 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
61 89 return split.call(str, separator, limit);
62 90 }
63 91 var output = [],
64 92 flags = (separator.ignoreCase ? "i" : "") +
65 93 (separator.multiline ? "m" : "") +
66 94 (separator.extended ? "x" : "") + // Proposed for ES6
67 95 (separator.sticky ? "y" : ""), // Firefox 3+
68 96 lastLastIndex = 0,
69 97 // Make `global` and avoid `lastIndex` issues by working with a copy
70 98 separator = new RegExp(separator.source, flags + "g"),
71 99 separator2, match, lastIndex, lastLength;
72 100 str += ""; // Type-convert
73 101
74 102 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
75 103 if (!compliantExecNpcg) {
76 104 // Doesn't need flags gy, but they don't hurt
77 105 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
78 106 }
79 107 /* Values for `limit`, per the spec:
80 108 * If undefined: 4294967295 // Math.pow(2, 32) - 1
81 109 * If 0, Infinity, or NaN: 0
82 110 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
83 111 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
84 112 * If other: Type-convert, then use the above rules
85 113 */
86 114 limit = typeof(limit) === "undefined" ?
87 115 -1 >>> 0 : // Math.pow(2, 32) - 1
88 116 limit >>> 0; // ToUint32(limit)
89 117 while (match = separator.exec(str)) {
90 118 // `separator.lastIndex` is not reliable cross-browser
91 119 lastIndex = match.index + match[0].length;
92 120 if (lastIndex > lastLastIndex) {
93 121 output.push(str.slice(lastLastIndex, match.index));
94 122 // Fix browsers whose `exec` methods don't consistently return `undefined` for
95 123 // nonparticipating capturing groups
96 124 if (!compliantExecNpcg && match.length > 1) {
97 125 match[0].replace(separator2, function () {
98 126 for (var i = 1; i < arguments.length - 2; i++) {
99 127 if (typeof(arguments[i]) === "undefined") {
100 128 match[i] = undefined;
101 129 }
102 130 }
103 131 });
104 132 }
105 133 if (match.length > 1 && match.index < str.length) {
106 134 Array.prototype.push.apply(output, match.slice(1));
107 135 }
108 136 lastLength = match[0].length;
109 137 lastLastIndex = lastIndex;
110 138 if (output.length >= limit) {
111 139 break;
112 140 }
113 141 }
114 142 if (separator.lastIndex === match.index) {
115 143 separator.lastIndex++; // Avoid an infinite loop
116 144 }
117 145 }
118 146 if (lastLastIndex === str.length) {
119 147 if (lastLength || !separator.test("")) {
120 148 output.push("");
121 149 }
122 150 } else {
123 151 output.push(str.slice(lastLastIndex));
124 152 }
125 153 return output.length > limit ? output.slice(0, limit) : output;
126 154 };
127 155
128 156 //============================================================================
129 157 // End contributed Cross-browser RegEx Split
130 158 //============================================================================
131 159
132 160
133 161 var uuid = function () {
134 162 // http://www.ietf.org/rfc/rfc4122.txt
135 163 var s = [];
136 164 var hexDigits = "0123456789ABCDEF";
137 165 for (var i = 0; i < 32; i++) {
138 166 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
139 167 }
140 168 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
141 169 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
142 170
143 171 var uuid = s.join("");
144 172 return uuid;
145 173 };
146 174
147 175
148 176 //Fix raw text to parse correctly in crazy XML
149 177 function xmlencode(string) {
150 178 return string.replace(/\&/g,'&'+'amp;')
151 179 .replace(/</g,'&'+'lt;')
152 180 .replace(/>/g,'&'+'gt;')
153 181 .replace(/\'/g,'&'+'apos;')
154 182 .replace(/\"/g,'&'+'quot;')
155 183 .replace(/`/g,'&'+'#96;');
156 184 }
157 185
158 186
159 187 //Map from terminal commands to CSS classes
160 188 var ansi_colormap = {
161 189 "01":"ansibold",
162 190
163 191 "30":"ansiblack",
164 192 "31":"ansired",
165 193 "32":"ansigreen",
166 194 "33":"ansiyellow",
167 195 "34":"ansiblue",
168 196 "35":"ansipurple",
169 197 "36":"ansicyan",
170 198 "37":"ansigray",
171 199
172 200 "40":"ansibgblack",
173 201 "41":"ansibgred",
174 202 "42":"ansibggreen",
175 203 "43":"ansibgyellow",
176 204 "44":"ansibgblue",
177 205 "45":"ansibgpurple",
178 206 "46":"ansibgcyan",
179 207 "47":"ansibggray"
180 208 };
181 209
182 210 function _process_numbers(attrs, numbers) {
183 211 // process ansi escapes
184 212 var n = numbers.shift();
185 213 if (ansi_colormap[n]) {
186 214 if ( ! attrs["class"] ) {
187 215 attrs["class"] = ansi_colormap[n];
188 216 } else {
189 217 attrs["class"] += " " + ansi_colormap[n];
190 218 }
191 219 } else if (n == "38" || n == "48") {
192 220 // VT100 256 color or 24 bit RGB
193 221 if (numbers.length < 2) {
194 222 console.log("Not enough fields for VT100 color", numbers);
195 223 return;
196 224 }
197 225
198 226 var index_or_rgb = numbers.shift();
199 227 var r,g,b;
200 228 if (index_or_rgb == "5") {
201 229 // 256 color
202 230 var idx = parseInt(numbers.shift());
203 231 if (idx < 16) {
204 232 // indexed ANSI
205 233 // ignore bright / non-bright distinction
206 234 idx = idx % 8;
207 235 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
208 236 if ( ! attrs["class"] ) {
209 237 attrs["class"] = ansiclass;
210 238 } else {
211 239 attrs["class"] += " " + ansiclass;
212 240 }
213 241 return;
214 242 } else if (idx < 232) {
215 243 // 216 color 6x6x6 RGB
216 244 idx = idx - 16;
217 245 b = idx % 6;
218 246 g = Math.floor(idx / 6) % 6;
219 247 r = Math.floor(idx / 36) % 6;
220 248 // convert to rgb
221 249 r = (r * 51);
222 250 g = (g * 51);
223 251 b = (b * 51);
224 252 } else {
225 253 // grayscale
226 254 idx = idx - 231;
227 255 // it's 1-24 and should *not* include black or white,
228 256 // so a 26 point scale
229 257 r = g = b = Math.floor(idx * 256 / 26);
230 258 }
231 259 } else if (index_or_rgb == "2") {
232 260 // Simple 24 bit RGB
233 261 if (numbers.length > 3) {
234 262 console.log("Not enough fields for RGB", numbers);
235 263 return;
236 264 }
237 265 r = numbers.shift();
238 266 g = numbers.shift();
239 267 b = numbers.shift();
240 268 } else {
241 269 console.log("unrecognized control", numbers);
242 270 return;
243 271 }
244 272 if (r !== undefined) {
245 273 // apply the rgb color
246 274 var line;
247 275 if (n == "38") {
248 276 line = "color: ";
249 277 } else {
250 278 line = "background-color: ";
251 279 }
252 280 line = line + "rgb(" + r + "," + g + "," + b + ");"
253 281 if ( !attrs["style"] ) {
254 282 attrs["style"] = line;
255 283 } else {
256 284 attrs["style"] += " " + line;
257 285 }
258 286 }
259 287 }
260 288 }
261 289
262 290 function ansispan(str) {
263 291 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
264 292 // regular ansi escapes (using the table above)
265 293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
266 294 if (!pattern) {
267 295 // [(01|22|39|)m close spans
268 296 return "</span>";
269 297 }
270 298 // consume sequence of color escapes
271 299 var numbers = pattern.match(/\d+/g);
272 300 var attrs = {};
273 301 while (numbers.length > 0) {
274 302 _process_numbers(attrs, numbers);
275 303 }
276 304
277 305 var span = "<span ";
278 306 for (var attr in attrs) {
279 307 var value = attrs[attr];
280 308 span = span + " " + attr + '="' + attrs[attr] + '"';
281 309 }
282 310 return span + ">";
283 311 });
284 312 };
285 313
286 314 // Transform ANSI color escape codes into HTML <span> tags with css
287 315 // classes listed in the above ansi_colormap object. The actual color used
288 316 // are set in the css file.
289 317 function fixConsole(txt) {
290 318 txt = xmlencode(txt);
291 319 var re = /\033\[([\dA-Fa-f;]*?)m/;
292 320 var opened = false;
293 321 var cmds = [];
294 322 var opener = "";
295 323 var closer = "";
296 324
297 325 // Strip all ANSI codes that are not color related. Matches
298 326 // all ANSI codes that do not end with "m".
299 327 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
300 328 txt = txt.replace(ignored_re, "");
301 329
302 330 // color ansi codes
303 331 txt = ansispan(txt);
304 332 return txt;
305 333 }
306 334
307 335 // Remove chunks that should be overridden by the effect of
308 336 // carriage return characters
309 337 function fixCarriageReturn(txt) {
310 338 var tmp = txt;
311 339 do {
312 340 txt = tmp;
313 341 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
314 342 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
315 343 } while (tmp.length < txt.length);
316 344 return txt;
317 345 }
318 346
319 347 // Locate any URLs and convert them to a anchor tag
320 348 function autoLinkUrls(txt) {
321 349 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
322 350 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
323 351 }
324 352
325 353 // some keycodes that seem to be platform/browser independent
326 354 var keycodes = {
327 355 BACKSPACE: 8,
328 356 TAB : 9,
329 357 ENTER : 13,
330 358 SHIFT : 16,
331 359 CTRL : 17,
332 360 CONTROL : 17,
333 361 ALT : 18,
334 362 CAPS_LOCK: 20,
335 363 ESC : 27,
336 364 SPACE : 32,
337 365 PGUP : 33,
338 366 PGDOWN : 34,
339 367 END : 35,
340 368 HOME : 36,
341 369 LEFT_ARROW: 37,
342 370 LEFTARROW: 37,
343 371 LEFT : 37,
344 372 UP_ARROW : 38,
345 373 UPARROW : 38,
346 374 UP : 38,
347 375 RIGHT_ARROW:39,
348 376 RIGHTARROW:39,
349 377 RIGHT : 39,
350 378 DOWN_ARROW: 40,
351 379 DOWNARROW: 40,
352 380 DOWN : 40,
353 381 I : 73,
354 382 M : 77,
355 383 // all three of these keys may be COMMAND on OS X:
356 384 LEFT_SUPER : 91,
357 385 RIGHT_SUPER : 92,
358 386 COMMAND : 93,
359 387 };
360 388
361 389 // trigger a key press event
362 390 var press = function (key) {
363 391 var key_press = $.Event('keydown', {which: key});
364 392 $(document).trigger(key_press);
365 393 }
366 394
367 395 var press_up = function() { press(keycodes.UP); };
368 396 var press_down = function() { press(keycodes.DOWN); };
369 397
370 398 var press_ctrl_enter = function() {
371 399 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, ctrlKey: true}));
372 400 };
373 401
374 402 var press_shift_enter = function() {
375 403 $(document).trigger($.Event('keydown', {which: keycodes.ENTER, shiftKey: true}));
376 404 };
377 405
378 406 // trigger the ctrl-m shortcut followed by one of our keys
379 407 var press_ghetto = function(key) {
380 408 $(document).trigger($.Event('keydown', {which: keycodes.M, ctrlKey: true}));
381 409 press(key);
382 410 };
383 411
384 412
385 413 var points_to_pixels = function (points) {
386 414 // A reasonably good way of converting between points and pixels.
387 415 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
388 416 $(body).append(test);
389 417 var pixel_per_point = test.width()/10000;
390 418 test.remove();
391 419 return Math.floor(points*pixel_per_point);
392 420 };
393 421
394 422 var always_new = function (constructor) {
395 423 // wrapper around contructor to avoid requiring `var a = new constructor()`
396 424 // useful for passing constructors as callbacks,
397 425 // not for programmer laziness.
398 426 // from http://programmers.stackexchange.com/questions/118798
399 427 return function () {
400 428 var obj = Object.create(constructor.prototype);
401 429 constructor.apply(obj, arguments);
402 430 return obj;
403 431 };
404 432 };
405 433
406 434
407 435 var url_path_join = function () {
408 436 // join a sequence of url components with '/'
409 437 var url = '';
410 438 for (var i = 0; i < arguments.length; i++) {
411 439 if (arguments[i] === '') {
412 440 continue;
413 441 }
414 442 if (url.length > 0 && url[url.length-1] != '/') {
415 443 url = url + '/' + arguments[i];
416 444 } else {
417 445 url = url + arguments[i];
418 446 }
419 447 }
420 448 url = url.replace(/\/\/+/, '/');
421 449 return url;
422 450 };
423 451
424 452 var parse_url = function (url) {
425 453 // an `a` element with an href allows attr-access to the parsed segments of a URL
426 454 // a = parse_url("http://localhost:8888/path/name#hash")
427 455 // a.protocol = "http:"
428 456 // a.host = "localhost:8888"
429 457 // a.hostname = "localhost"
430 458 // a.port = 8888
431 459 // a.pathname = "/path/name"
432 460 // a.hash = "#hash"
433 461 var a = document.createElement("a");
434 462 a.href = url;
435 463 return a;
436 464 };
437 465
438 466 var encode_uri_components = function (uri) {
439 467 // encode just the components of a multi-segment uri,
440 468 // leaving '/' separators
441 469 return uri.split('/').map(encodeURIComponent).join('/');
442 470 };
443 471
444 472 var url_join_encode = function () {
445 473 // join a sequence of url components with '/',
446 474 // encoding each component with encodeURIComponent
447 475 return encode_uri_components(url_path_join.apply(null, arguments));
448 476 };
449 477
450 478
451 479 var splitext = function (filename) {
452 480 // mimic Python os.path.splitext
453 481 // Returns ['base', '.ext']
454 482 var idx = filename.lastIndexOf('.');
455 483 if (idx > 0) {
456 484 return [filename.slice(0, idx), filename.slice(idx)];
457 485 } else {
458 486 return [filename, ''];
459 487 }
460 488 };
461 489
462 490
463 491 var get_body_data = function(key) {
464 492 // get a url-encoded item from body.data and decode it
465 493 // we should never have any encoded URLs anywhere else in code
466 494 // until we are building an actual request
467 495 return decodeURIComponent($('body').data(key));
468 496 };
469 497
470 498
471 499 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
472 500 var browser = (function() {
473 501 if (typeof navigator === 'undefined') {
474 502 // navigator undefined in node
475 503 return 'None';
476 504 }
477 505 var N= navigator.appName, ua= navigator.userAgent, tem;
478 506 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
479 507 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
480 508 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
481 509 return M;
482 510 })();
483 511
484 512 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
485 513 var platform = (function () {
486 514 if (typeof navigator === 'undefined') {
487 515 // navigator undefined in node
488 516 return 'None';
489 517 }
490 518 var OSName="None";
491 519 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
492 520 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
493 521 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
494 522 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
495 523 return OSName
496 524 })();
497 525
498 526 var is_or_has = function (a, b) {
499 527 // Is b a child of a or a itself?
500 528 return a.has(b).length !==0 || a.is(b);
501 529 }
502 530
503 531 var is_focused = function (e) {
504 532 // Is element e, or one of its children focused?
505 533 e = $(e);
506 534 var target = $(document.activeElement);
507 535 if (target.length > 0) {
508 536 if (is_or_has(e, target)) {
509 537 return true;
510 538 } else {
511 539 return false;
512 540 }
513 541 } else {
514 542 return false;
515 543 }
516 544 }
517 545
518 546
519 547 return {
520 548 regex_split : regex_split,
521 549 uuid : uuid,
522 550 fixConsole : fixConsole,
523 551 keycodes : keycodes,
524 552 press : press,
525 553 press_up : press_up,
526 554 press_down : press_down,
527 555 press_ctrl_enter : press_ctrl_enter,
528 556 press_shift_enter : press_shift_enter,
529 557 press_ghetto : press_ghetto,
530 558 fixCarriageReturn : fixCarriageReturn,
531 559 autoLinkUrls : autoLinkUrls,
532 560 points_to_pixels : points_to_pixels,
533 561 get_body_data : get_body_data,
534 562 parse_url : parse_url,
535 563 url_path_join : url_path_join,
536 564 url_join_encode : url_join_encode,
537 565 encode_uri_components : encode_uri_components,
538 566 splitext : splitext,
539 567 always_new : always_new,
540 568 browser : browser,
541 569 platform: platform,
542 570 is_or_has : is_or_has,
543 571 is_focused : is_focused
544 572 };
545 573
546 574 }(IPython));
547 575
@@ -1,391 +1,395 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 The :class:`~IPython.core.application.Application` object for the command
5 5 line :command:`ipython` program.
6 6
7 7 Authors
8 8 -------
9 9
10 10 * Brian Granger
11 11 * Fernando Perez
12 12 * Min Ragan-Kelley
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Copyright (C) 2008-2011 The IPython Development Team
17 17 #
18 18 # Distributed under the terms of the BSD License. The full license is in
19 19 # the file COPYING, distributed as part of this software.
20 20 #-----------------------------------------------------------------------------
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Imports
24 24 #-----------------------------------------------------------------------------
25 25
26 26 from __future__ import absolute_import
27 27 from __future__ import print_function
28 28
29 29 import logging
30 30 import os
31 31 import sys
32 32
33 33 from IPython.config.loader import (
34 34 Config, PyFileConfigLoader, ConfigFileNotFound
35 35 )
36 36 from IPython.config.application import boolean_flag, catch_config_error, Application
37 37 from IPython.core import release
38 38 from IPython.core import usage
39 39 from IPython.core.completer import IPCompleter
40 40 from IPython.core.crashhandler import CrashHandler
41 41 from IPython.core.formatters import PlainTextFormatter
42 42 from IPython.core.history import HistoryManager
43 43 from IPython.core.prompts import PromptManager
44 44 from IPython.core.application import (
45 45 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
46 46 )
47 47 from IPython.core.magics import ScriptMagics
48 48 from IPython.core.shellapp import (
49 49 InteractiveShellApp, shell_flags, shell_aliases
50 50 )
51 51 from IPython.extensions.storemagic import StoreMagics
52 52 from IPython.terminal.interactiveshell import TerminalInteractiveShell
53 53 from IPython.utils import warn
54 54 from IPython.utils.path import get_ipython_dir, check_for_old_config
55 55 from IPython.utils.traitlets import (
56 56 Bool, List, Dict,
57 57 )
58 58
59 59 #-----------------------------------------------------------------------------
60 60 # Globals, utilities and helpers
61 61 #-----------------------------------------------------------------------------
62 62
63 63 _examples = """
64 64 ipython --matplotlib # enable matplotlib integration
65 65 ipython --matplotlib=qt # enable matplotlib integration with qt4 backend
66 66
67 67 ipython --log-level=DEBUG # set logging to DEBUG
68 68 ipython --profile=foo # start with profile foo
69 69
70 70 ipython qtconsole # start the qtconsole GUI application
71 71 ipython help qtconsole # show the help for the qtconsole subcmd
72 72
73 73 ipython console # start the terminal-based console application
74 74 ipython help console # show the help for the console subcmd
75 75
76 76 ipython notebook # start the IPython notebook
77 77 ipython help notebook # show the help for the notebook subcmd
78 78
79 79 ipython profile create foo # create profile foo w/ default config files
80 80 ipython help profile # show the help for the profile subcmd
81 81
82 82 ipython locate # print the path to the IPython directory
83 83 ipython locate profile foo # print the path to the directory for profile `foo`
84 84
85 85 ipython nbconvert # convert notebooks to/from other formats
86 86 """
87 87
88 88 #-----------------------------------------------------------------------------
89 89 # Crash handler for this application
90 90 #-----------------------------------------------------------------------------
91 91
92 92 class IPAppCrashHandler(CrashHandler):
93 93 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
94 94
95 95 def __init__(self, app):
96 96 contact_name = release.author
97 97 contact_email = release.author_email
98 98 bug_tracker = 'https://github.com/ipython/ipython/issues'
99 99 super(IPAppCrashHandler,self).__init__(
100 100 app, contact_name, contact_email, bug_tracker
101 101 )
102 102
103 103 def make_report(self,traceback):
104 104 """Return a string containing a crash report."""
105 105
106 106 sec_sep = self.section_sep
107 107 # Start with parent report
108 108 report = [super(IPAppCrashHandler, self).make_report(traceback)]
109 109 # Add interactive-specific info we may have
110 110 rpt_add = report.append
111 111 try:
112 112 rpt_add(sec_sep+"History of session input:")
113 113 for line in self.app.shell.user_ns['_ih']:
114 114 rpt_add(line)
115 115 rpt_add('\n*** Last line of input (may not be in above history):\n')
116 116 rpt_add(self.app.shell._last_input_line+'\n')
117 117 except:
118 118 pass
119 119
120 120 return ''.join(report)
121 121
122 122 #-----------------------------------------------------------------------------
123 123 # Aliases and Flags
124 124 #-----------------------------------------------------------------------------
125 125 flags = dict(base_flags)
126 126 flags.update(shell_flags)
127 127 frontend_flags = {}
128 128 addflag = lambda *args: frontend_flags.update(boolean_flag(*args))
129 129 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
130 130 'Turn on auto editing of files with syntax errors.',
131 131 'Turn off auto editing of files with syntax errors.'
132 132 )
133 133 addflag('banner', 'TerminalIPythonApp.display_banner',
134 134 "Display a banner upon starting IPython.",
135 135 "Don't display a banner upon starting IPython."
136 136 )
137 137 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
138 138 """Set to confirm when you try to exit IPython with an EOF (Control-D
139 139 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
140 140 you can force a direct exit without any confirmation.""",
141 141 "Don't prompt the user when exiting."
142 142 )
143 143 addflag('term-title', 'TerminalInteractiveShell.term_title',
144 144 "Enable auto setting the terminal title.",
145 145 "Disable auto setting the terminal title."
146 146 )
147 147 classic_config = Config()
148 148 classic_config.InteractiveShell.cache_size = 0
149 149 classic_config.PlainTextFormatter.pprint = False
150 150 classic_config.PromptManager.in_template = '>>> '
151 151 classic_config.PromptManager.in2_template = '... '
152 152 classic_config.PromptManager.out_template = ''
153 153 classic_config.InteractiveShell.separate_in = ''
154 154 classic_config.InteractiveShell.separate_out = ''
155 155 classic_config.InteractiveShell.separate_out2 = ''
156 156 classic_config.InteractiveShell.colors = 'NoColor'
157 157 classic_config.InteractiveShell.xmode = 'Plain'
158 158
159 159 frontend_flags['classic']=(
160 160 classic_config,
161 161 "Gives IPython a similar feel to the classic Python prompt."
162 162 )
163 163 # # log doesn't make so much sense this way anymore
164 164 # paa('--log','-l',
165 165 # action='store_true', dest='InteractiveShell.logstart',
166 166 # help="Start logging to the default log file (./ipython_log.py).")
167 167 #
168 168 # # quick is harder to implement
169 169 frontend_flags['quick']=(
170 170 {'TerminalIPythonApp' : {'quick' : True}},
171 171 "Enable quick startup with no config files."
172 172 )
173 173
174 174 frontend_flags['i'] = (
175 175 {'TerminalIPythonApp' : {'force_interact' : True}},
176 176 """If running code from the command line, become interactive afterwards.
177 177 Note: can also be given simply as '-i'."""
178 178 )
179 179 flags.update(frontend_flags)
180 180
181 181 aliases = dict(base_aliases)
182 182 aliases.update(shell_aliases)
183 183
184 184 #-----------------------------------------------------------------------------
185 185 # Main classes and functions
186 186 #-----------------------------------------------------------------------------
187 187
188 188
189 189 class LocateIPythonApp(BaseIPythonApplication):
190 190 description = """print the path to the IPython dir"""
191 191 subcommands = Dict(dict(
192 192 profile=('IPython.core.profileapp.ProfileLocate',
193 193 "print the path to an IPython profile directory",
194 194 ),
195 195 ))
196 196 def start(self):
197 197 if self.subapp is not None:
198 198 return self.subapp.start()
199 199 else:
200 200 print(self.ipython_dir)
201 201
202 202
203 203 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
204 204 name = u'ipython'
205 205 description = usage.cl_usage
206 206 crash_handler_class = IPAppCrashHandler
207 207 examples = _examples
208 208
209 209 flags = Dict(flags)
210 210 aliases = Dict(aliases)
211 211 classes = List()
212 212 def _classes_default(self):
213 213 """This has to be in a method, for TerminalIPythonApp to be available."""
214 214 return [
215 215 InteractiveShellApp, # ShellApp comes before TerminalApp, because
216 216 self.__class__, # it will also affect subclasses (e.g. QtConsole)
217 217 TerminalInteractiveShell,
218 218 PromptManager,
219 219 HistoryManager,
220 220 ProfileDir,
221 221 PlainTextFormatter,
222 222 IPCompleter,
223 223 ScriptMagics,
224 224 StoreMagics,
225 225 ]
226 226
227 subcommands = Dict(dict(
227 subcommands = dict(
228 228 qtconsole=('IPython.qt.console.qtconsoleapp.IPythonQtConsoleApp',
229 229 """Launch the IPython Qt Console."""
230 230 ),
231 231 notebook=('IPython.html.notebookapp.NotebookApp',
232 232 """Launch the IPython HTML Notebook Server."""
233 233 ),
234 234 profile = ("IPython.core.profileapp.ProfileApp",
235 235 "Create and manage IPython profiles."
236 236 ),
237 237 kernel = ("IPython.kernel.zmq.kernelapp.IPKernelApp",
238 238 "Start a kernel without an attached frontend."
239 239 ),
240 240 console=('IPython.terminal.console.app.ZMQTerminalIPythonApp',
241 241 """Launch the IPython terminal-based Console."""
242 242 ),
243 243 locate=('IPython.terminal.ipapp.LocateIPythonApp',
244 244 LocateIPythonApp.description
245 245 ),
246 246 history=('IPython.core.historyapp.HistoryApp',
247 247 "Manage the IPython history database."
248 248 ),
249 249 nbconvert=('IPython.nbconvert.nbconvertapp.NbConvertApp',
250 250 "Convert notebooks to/from other formats."
251 251 ),
252 252 trust=('IPython.nbformat.sign.TrustNotebookApp',
253 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 261 # *do* autocreate requested profile, but don't create the config file.
258 262 auto_create=Bool(True)
259 263 # configurables
260 264 ignore_old_config=Bool(False, config=True,
261 265 help="Suppress warning messages about legacy config files"
262 266 )
263 267 quick = Bool(False, config=True,
264 268 help="""Start IPython quickly by skipping the loading of config files."""
265 269 )
266 270 def _quick_changed(self, name, old, new):
267 271 if new:
268 272 self.load_config_file = lambda *a, **kw: None
269 273 self.ignore_old_config=True
270 274
271 275 display_banner = Bool(True, config=True,
272 276 help="Whether to display a banner upon starting IPython."
273 277 )
274 278
275 279 # if there is code of files to run from the cmd line, don't interact
276 280 # unless the --i flag (App.force_interact) is true.
277 281 force_interact = Bool(False, config=True,
278 282 help="""If a command or file is given via the command-line,
279 283 e.g. 'ipython foo.py', start an interactive shell after executing the
280 284 file or command."""
281 285 )
282 286 def _force_interact_changed(self, name, old, new):
283 287 if new:
284 288 self.interact = True
285 289
286 290 def _file_to_run_changed(self, name, old, new):
287 291 if new:
288 292 self.something_to_run = True
289 293 if new and not self.force_interact:
290 294 self.interact = False
291 295 _code_to_run_changed = _file_to_run_changed
292 296 _module_to_run_changed = _file_to_run_changed
293 297
294 298 # internal, not-configurable
295 299 interact=Bool(True)
296 300 something_to_run=Bool(False)
297 301
298 302 def parse_command_line(self, argv=None):
299 303 """override to allow old '-pylab' flag with deprecation warning"""
300 304
301 305 argv = sys.argv[1:] if argv is None else argv
302 306
303 307 if '-pylab' in argv:
304 308 # deprecated `-pylab` given,
305 309 # warn and transform into current syntax
306 310 argv = argv[:] # copy, don't clobber
307 311 idx = argv.index('-pylab')
308 312 warn.warn("`-pylab` flag has been deprecated.\n"
309 313 " Use `--matplotlib <backend>` and import pylab manually.")
310 314 argv[idx] = '--pylab'
311 315
312 316 return super(TerminalIPythonApp, self).parse_command_line(argv)
313 317
314 318 @catch_config_error
315 319 def initialize(self, argv=None):
316 320 """Do actions after construct, but before starting the app."""
317 321 super(TerminalIPythonApp, self).initialize(argv)
318 322 if self.subapp is not None:
319 323 # don't bother initializing further, starting subapp
320 324 return
321 325 if not self.ignore_old_config:
322 326 check_for_old_config(self.ipython_dir)
323 327 # print self.extra_args
324 328 if self.extra_args and not self.something_to_run:
325 329 self.file_to_run = self.extra_args[0]
326 330 self.init_path()
327 331 # create the shell
328 332 self.init_shell()
329 333 # and draw the banner
330 334 self.init_banner()
331 335 # Now a variety of things that happen after the banner is printed.
332 336 self.init_gui_pylab()
333 337 self.init_extensions()
334 338 self.init_code()
335 339
336 340 def init_shell(self):
337 341 """initialize the InteractiveShell instance"""
338 342 # Create an InteractiveShell instance.
339 343 # shell.display_banner should always be False for the terminal
340 344 # based app, because we call shell.show_banner() by hand below
341 345 # so the banner shows *before* all extension loading stuff.
342 346 self.shell = TerminalInteractiveShell.instance(parent=self,
343 347 display_banner=False, profile_dir=self.profile_dir,
344 348 ipython_dir=self.ipython_dir, user_ns=self.user_ns)
345 349 self.shell.configurables.append(self)
346 350
347 351 def init_banner(self):
348 352 """optionally display the banner"""
349 353 if self.display_banner and self.interact:
350 354 self.shell.show_banner()
351 355 # Make sure there is a space below the banner.
352 356 if self.log_level <= logging.INFO: print()
353 357
354 358 def _pylab_changed(self, name, old, new):
355 359 """Replace --pylab='inline' with --pylab='auto'"""
356 360 if new == 'inline':
357 361 warn.warn("'inline' not available as pylab backend, "
358 362 "using 'auto' instead.")
359 363 self.pylab = 'auto'
360 364
361 365 def start(self):
362 366 if self.subapp is not None:
363 367 return self.subapp.start()
364 368 # perform any prexec steps:
365 369 if self.interact:
366 370 self.log.debug("Starting IPython's mainloop...")
367 371 self.shell.mainloop()
368 372 else:
369 373 self.log.debug("IPython not interactive...")
370 374
371 375 def load_default_config(ipython_dir=None):
372 376 """Load the default config file from the default ipython_dir.
373 377
374 378 This is useful for embedded shells.
375 379 """
376 380 if ipython_dir is None:
377 381 ipython_dir = get_ipython_dir()
378 382
379 383 profile_dir = os.path.join(ipython_dir, 'profile_default')
380 384
381 385 config = Config()
382 386 for cf in Application._load_config_files("ipython_config", path=profile_dir):
383 387 config.update(cf)
384 388
385 389 return config
386 390
387 391 launch_new_instance = TerminalIPythonApp.launch_instance
388 392
389 393
390 394 if __name__ == '__main__':
391 395 launch_new_instance()
@@ -1,446 +1,454 b''
1 1 """Generic testing tools.
2 2
3 3 Authors
4 4 -------
5 5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 6 """
7 7
8 8 from __future__ import absolute_import
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2009 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 import os
22 22 import re
23 23 import sys
24 24 import tempfile
25 25
26 26 from contextlib import contextmanager
27 27 from io import StringIO
28 28 from subprocess import Popen, PIPE
29 29
30 30 try:
31 31 # These tools are used by parts of the runtime, so we make the nose
32 32 # dependency optional at this point. Nose is a hard dependency to run the
33 33 # test suite, but NOT to use ipython itself.
34 34 import nose.tools as nt
35 35 has_nose = True
36 36 except ImportError:
37 37 has_nose = False
38 38
39 39 from IPython.config.loader import Config
40 40 from IPython.utils.process import get_output_error_code
41 41 from IPython.utils.text import list_strings
42 42 from IPython.utils.io import temp_pyfile, Tee
43 43 from IPython.utils import py3compat
44 44 from IPython.utils.encoding import DEFAULT_ENCODING
45 45
46 46 from . import decorators as dec
47 47 from . import skipdoctest
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Functions and classes
51 51 #-----------------------------------------------------------------------------
52 52
53 53 # The docstring for full_path doctests differently on win32 (different path
54 54 # separator) so just skip the doctest there. The example remains informative.
55 55 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
56 56
57 57 @doctest_deco
58 58 def full_path(startPath,files):
59 59 """Make full paths for all the listed files, based on startPath.
60 60
61 61 Only the base part of startPath is kept, since this routine is typically
62 62 used with a script's ``__file__`` variable as startPath. The base of startPath
63 63 is then prepended to all the listed files, forming the output list.
64 64
65 65 Parameters
66 66 ----------
67 67 startPath : string
68 68 Initial path to use as the base for the results. This path is split
69 69 using os.path.split() and only its first component is kept.
70 70
71 71 files : string or list
72 72 One or more files.
73 73
74 74 Examples
75 75 --------
76 76
77 77 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
78 78 ['/foo/a.txt', '/foo/b.txt']
79 79
80 80 >>> full_path('/foo',['a.txt','b.txt'])
81 81 ['/a.txt', '/b.txt']
82 82
83 83 If a single file is given, the output is still a list::
84 84
85 85 >>> full_path('/foo','a.txt')
86 86 ['/a.txt']
87 87 """
88 88
89 89 files = list_strings(files)
90 90 base = os.path.split(startPath)[0]
91 91 return [ os.path.join(base,f) for f in files ]
92 92
93 93
94 94 def parse_test_output(txt):
95 95 """Parse the output of a test run and return errors, failures.
96 96
97 97 Parameters
98 98 ----------
99 99 txt : str
100 100 Text output of a test run, assumed to contain a line of one of the
101 101 following forms::
102 102
103 103 'FAILED (errors=1)'
104 104 'FAILED (failures=1)'
105 105 'FAILED (errors=1, failures=1)'
106 106
107 107 Returns
108 108 -------
109 109 nerr, nfail
110 110 number of errors and failures.
111 111 """
112 112
113 113 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
114 114 if err_m:
115 115 nerr = int(err_m.group(1))
116 116 nfail = 0
117 117 return nerr, nfail
118 118
119 119 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
120 120 if fail_m:
121 121 nerr = 0
122 122 nfail = int(fail_m.group(1))
123 123 return nerr, nfail
124 124
125 125 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
126 126 re.MULTILINE)
127 127 if both_m:
128 128 nerr = int(both_m.group(1))
129 129 nfail = int(both_m.group(2))
130 130 return nerr, nfail
131 131
132 132 # If the input didn't match any of these forms, assume no error/failures
133 133 return 0, 0
134 134
135 135
136 136 # So nose doesn't think this is a test
137 137 parse_test_output.__test__ = False
138 138
139 139
140 140 def default_argv():
141 141 """Return a valid default argv for creating testing instances of ipython"""
142 142
143 143 return ['--quick', # so no config file is loaded
144 144 # Other defaults to minimize side effects on stdout
145 145 '--colors=NoColor', '--no-term-title','--no-banner',
146 146 '--autocall=0']
147 147
148 148
149 149 def default_config():
150 150 """Return a config object with good defaults for testing."""
151 151 config = Config()
152 152 config.TerminalInteractiveShell.colors = 'NoColor'
153 153 config.TerminalTerminalInteractiveShell.term_title = False,
154 154 config.TerminalInteractiveShell.autocall = 0
155 155 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
156 156 config.HistoryManager.db_cache_size = 10000
157 157 return config
158 158
159 159
160 160 def get_ipython_cmd(as_string=False):
161 161 """
162 162 Return appropriate IPython command line name. By default, this will return
163 163 a list that can be used with subprocess.Popen, for example, but passing
164 164 `as_string=True` allows for returning the IPython command as a string.
165 165
166 166 Parameters
167 167 ----------
168 168 as_string: bool
169 169 Flag to allow to return the command as a string.
170 170 """
171 171 ipython_cmd = [sys.executable, "-m", "IPython"]
172 172
173 173 if as_string:
174 174 ipython_cmd = " ".join(ipython_cmd)
175 175
176 176 return ipython_cmd
177 177
178 178 def ipexec(fname, options=None):
179 179 """Utility to call 'ipython filename'.
180 180
181 181 Starts IPython with a minimal and safe configuration to make startup as fast
182 182 as possible.
183 183
184 184 Note that this starts IPython in a subprocess!
185 185
186 186 Parameters
187 187 ----------
188 188 fname : str
189 189 Name of file to be executed (should have .py or .ipy extension).
190 190
191 191 options : optional, list
192 192 Extra command-line flags to be passed to IPython.
193 193
194 194 Returns
195 195 -------
196 196 (stdout, stderr) of ipython subprocess.
197 197 """
198 198 if options is None: options = []
199 199
200 200 # For these subprocess calls, eliminate all prompt printing so we only see
201 201 # output from script execution
202 202 prompt_opts = [ '--PromptManager.in_template=""',
203 203 '--PromptManager.in2_template=""',
204 204 '--PromptManager.out_template=""'
205 205 ]
206 206 cmdargs = default_argv() + prompt_opts + options
207 207
208 208 test_dir = os.path.dirname(__file__)
209 209
210 210 ipython_cmd = get_ipython_cmd()
211 211 # Absolute path for filename
212 212 full_fname = os.path.join(test_dir, fname)
213 213 full_cmd = ipython_cmd + cmdargs + [full_fname]
214 214 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE)
215 215 out, err = p.communicate()
216 216 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
217 217 # `import readline` causes 'ESC[?1034h' to be output sometimes,
218 218 # so strip that out before doing comparisons
219 219 if out:
220 220 out = re.sub(r'\x1b\[[^h]+h', '', out)
221 221 return out, err
222 222
223 223
224 224 def ipexec_validate(fname, expected_out, expected_err='',
225 225 options=None):
226 226 """Utility to call 'ipython filename' and validate output/error.
227 227
228 228 This function raises an AssertionError if the validation fails.
229 229
230 230 Note that this starts IPython in a subprocess!
231 231
232 232 Parameters
233 233 ----------
234 234 fname : str
235 235 Name of the file to be executed (should have .py or .ipy extension).
236 236
237 237 expected_out : str
238 238 Expected stdout of the process.
239 239
240 240 expected_err : optional, str
241 241 Expected stderr of the process.
242 242
243 243 options : optional, list
244 244 Extra command-line flags to be passed to IPython.
245 245
246 246 Returns
247 247 -------
248 248 None
249 249 """
250 250
251 251 import nose.tools as nt
252 252
253 253 out, err = ipexec(fname, options)
254 254 #print 'OUT', out # dbg
255 255 #print 'ERR', err # dbg
256 256 # If there are any errors, we must check those befor stdout, as they may be
257 257 # more informative than simply having an empty stdout.
258 258 if err:
259 259 if expected_err:
260 260 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
261 261 else:
262 262 raise ValueError('Running file %r produced error: %r' %
263 263 (fname, err))
264 264 # If no errors or output on stderr was expected, match stdout
265 265 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
266 266
267 267
268 268 class TempFileMixin(object):
269 269 """Utility class to create temporary Python/IPython files.
270 270
271 271 Meant as a mixin class for test cases."""
272 272
273 273 def mktmp(self, src, ext='.py'):
274 274 """Make a valid python temp file."""
275 275 fname, f = temp_pyfile(src, ext)
276 276 self.tmpfile = f
277 277 self.fname = fname
278 278
279 279 def tearDown(self):
280 280 if hasattr(self, 'tmpfile'):
281 281 # If the tmpfile wasn't made because of skipped tests, like in
282 282 # win32, there's nothing to cleanup.
283 283 self.tmpfile.close()
284 284 try:
285 285 os.unlink(self.fname)
286 286 except:
287 287 # On Windows, even though we close the file, we still can't
288 288 # delete it. I have no clue why
289 289 pass
290 290
291 291 pair_fail_msg = ("Testing {0}\n\n"
292 292 "In:\n"
293 293 " {1!r}\n"
294 294 "Expected:\n"
295 295 " {2!r}\n"
296 296 "Got:\n"
297 297 " {3!r}\n")
298 298 def check_pairs(func, pairs):
299 299 """Utility function for the common case of checking a function with a
300 300 sequence of input/output pairs.
301 301
302 302 Parameters
303 303 ----------
304 304 func : callable
305 305 The function to be tested. Should accept a single argument.
306 306 pairs : iterable
307 307 A list of (input, expected_output) tuples.
308 308
309 309 Returns
310 310 -------
311 311 None. Raises an AssertionError if any output does not match the expected
312 312 value.
313 313 """
314 314 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
315 315 for inp, expected in pairs:
316 316 out = func(inp)
317 317 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
318 318
319 319
320 320 if py3compat.PY3:
321 321 MyStringIO = StringIO
322 322 else:
323 323 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
324 324 # so we need a class that can handle both.
325 325 class MyStringIO(StringIO):
326 326 def write(self, s):
327 327 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
328 328 super(MyStringIO, self).write(s)
329 329
330 _re_type = type(re.compile(r''))
331
330 332 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
331 333 -------
332 334 {2!s}
333 335 -------
334 336 """
335 337
336 338 class AssertPrints(object):
337 339 """Context manager for testing that code prints certain text.
338 340
339 341 Examples
340 342 --------
341 343 >>> with AssertPrints("abc", suppress=False):
342 344 ... print("abcd")
343 345 ... print("def")
344 346 ...
345 347 abcd
346 348 def
347 349 """
348 350 def __init__(self, s, channel='stdout', suppress=True):
349 351 self.s = s
350 if isinstance(self.s, py3compat.string_types):
352 if isinstance(self.s, (py3compat.string_types, _re_type)):
351 353 self.s = [self.s]
352 354 self.channel = channel
353 355 self.suppress = suppress
354 356
355 357 def __enter__(self):
356 358 self.orig_stream = getattr(sys, self.channel)
357 359 self.buffer = MyStringIO()
358 360 self.tee = Tee(self.buffer, channel=self.channel)
359 361 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
360 362
361 363 def __exit__(self, etype, value, traceback):
362 364 if value is not None:
363 365 # If an error was raised, don't check anything else
364 366 return False
365 367 self.tee.flush()
366 368 setattr(sys, self.channel, self.orig_stream)
367 369 printed = self.buffer.getvalue()
368 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 375 return False
371 376
372 377 printed_msg = """Found {0!r} in printed output (on {1}):
373 378 -------
374 379 {2!s}
375 380 -------
376 381 """
377 382
378 383 class AssertNotPrints(AssertPrints):
379 384 """Context manager for checking that certain output *isn't* produced.
380 385
381 386 Counterpart of AssertPrints"""
382 387 def __exit__(self, etype, value, traceback):
383 388 if value is not None:
384 389 # If an error was raised, don't check anything else
385 390 return False
386 391 self.tee.flush()
387 392 setattr(sys, self.channel, self.orig_stream)
388 393 printed = self.buffer.getvalue()
389 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 399 return False
392 400
393 401 @contextmanager
394 402 def mute_warn():
395 403 from IPython.utils import warn
396 404 save_warn = warn.warn
397 405 warn.warn = lambda *a, **kw: None
398 406 try:
399 407 yield
400 408 finally:
401 409 warn.warn = save_warn
402 410
403 411 @contextmanager
404 412 def make_tempfile(name):
405 413 """ Create an empty, named, temporary file for the duration of the context.
406 414 """
407 415 f = open(name, 'w')
408 416 f.close()
409 417 try:
410 418 yield
411 419 finally:
412 420 os.unlink(name)
413 421
414 422
415 423 @contextmanager
416 424 def monkeypatch(obj, name, attr):
417 425 """
418 426 Context manager to replace attribute named `name` in `obj` with `attr`.
419 427 """
420 428 orig = getattr(obj, name)
421 429 setattr(obj, name, attr)
422 430 yield
423 431 setattr(obj, name, orig)
424 432
425 433
426 434 def help_output_test(subcommand=''):
427 435 """test that `ipython [subcommand] -h` works"""
428 436 cmd = get_ipython_cmd() + [subcommand, '-h']
429 437 out, err, rc = get_output_error_code(cmd)
430 438 nt.assert_equal(rc, 0, err)
431 439 nt.assert_not_in("Traceback", err)
432 440 nt.assert_in("Options", out)
433 441 nt.assert_in("--help-all", out)
434 442 return out, err
435 443
436 444
437 445 def help_all_output_test(subcommand=''):
438 446 """test that `ipython [subcommand] --help-all` works"""
439 447 cmd = get_ipython_cmd() + [subcommand, '--help-all']
440 448 out, err, rc = get_output_error_code(cmd)
441 449 nt.assert_equal(rc, 0, err)
442 450 nt.assert_not_in("Traceback", err)
443 451 nt.assert_in("Options", out)
444 452 nt.assert_in("Class parameters", out)
445 453 return out, err
446 454
General Comments 0
You need to be logged in to leave comments. Login now