##// END OF EJS Templates
IPython/core/history.py: Handle RuntimeError on Thread.start (#14318)...
M Bussonnier -
r28625:0a3cf8f6 merge
parent child Browse files
Show More
@@ -1,978 +1,988 b''
1 """ History related magics and functionality """
1 """ History related magics and functionality """
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6
6
7 import atexit
7 import atexit
8 import datetime
8 import datetime
9 from pathlib import Path
10 import re
9 import re
11 import sqlite3
10 import sqlite3
12 import threading
11 import threading
12 from pathlib import Path
13
13
14 from traitlets.config.configurable import LoggingConfigurable
15 from decorator import decorator
14 from decorator import decorator
16 from IPython.utils.decorators import undoc
17 from IPython.paths import locate_profile
18 from traitlets import (
15 from traitlets import (
19 Any,
16 Any,
20 Bool,
17 Bool,
21 Dict,
18 Dict,
22 Instance,
19 Instance,
23 Integer,
20 Integer,
24 List,
21 List,
22 TraitError,
25 Unicode,
23 Unicode,
26 Union,
24 Union,
27 TraitError,
28 default,
25 default,
29 observe,
26 observe,
30 )
27 )
28 from traitlets.config.configurable import LoggingConfigurable
29
30 from IPython.paths import locate_profile
31 from IPython.utils.decorators import undoc
31
32
32 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
33 # Classes and functions
34 # Classes and functions
34 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
35
36
36 @undoc
37 @undoc
37 class DummyDB(object):
38 class DummyDB(object):
38 """Dummy DB that will act as a black hole for history.
39 """Dummy DB that will act as a black hole for history.
39
40
40 Only used in the absence of sqlite"""
41 Only used in the absence of sqlite"""
41 def execute(*args, **kwargs):
42 def execute(*args, **kwargs):
42 return []
43 return []
43
44
44 def commit(self, *args, **kwargs):
45 def commit(self, *args, **kwargs):
45 pass
46 pass
46
47
47 def __enter__(self, *args, **kwargs):
48 def __enter__(self, *args, **kwargs):
48 pass
49 pass
49
50
50 def __exit__(self, *args, **kwargs):
51 def __exit__(self, *args, **kwargs):
51 pass
52 pass
52
53
53
54
54 @decorator
55 @decorator
55 def only_when_enabled(f, self, *a, **kw):
56 def only_when_enabled(f, self, *a, **kw):
56 """Decorator: return an empty list in the absence of sqlite."""
57 """Decorator: return an empty list in the absence of sqlite."""
57 if not self.enabled:
58 if not self.enabled:
58 return []
59 return []
59 else:
60 else:
60 return f(self, *a, **kw)
61 return f(self, *a, **kw)
61
62
62
63
63 # use 16kB as threshold for whether a corrupt history db should be saved
64 # use 16kB as threshold for whether a corrupt history db should be saved
64 # that should be at least 100 entries or so
65 # that should be at least 100 entries or so
65 _SAVE_DB_SIZE = 16384
66 _SAVE_DB_SIZE = 16384
66
67
67 @decorator
68 @decorator
68 def catch_corrupt_db(f, self, *a, **kw):
69 def catch_corrupt_db(f, self, *a, **kw):
69 """A decorator which wraps HistoryAccessor method calls to catch errors from
70 """A decorator which wraps HistoryAccessor method calls to catch errors from
70 a corrupt SQLite database, move the old database out of the way, and create
71 a corrupt SQLite database, move the old database out of the way, and create
71 a new one.
72 a new one.
72
73
73 We avoid clobbering larger databases because this may be triggered due to filesystem issues,
74 We avoid clobbering larger databases because this may be triggered due to filesystem issues,
74 not just a corrupt file.
75 not just a corrupt file.
75 """
76 """
76 try:
77 try:
77 return f(self, *a, **kw)
78 return f(self, *a, **kw)
78 except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
79 except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
79 self._corrupt_db_counter += 1
80 self._corrupt_db_counter += 1
80 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e)
81 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e)
81 if self.hist_file != ':memory:':
82 if self.hist_file != ':memory:':
82 if self._corrupt_db_counter > self._corrupt_db_limit:
83 if self._corrupt_db_counter > self._corrupt_db_limit:
83 self.hist_file = ':memory:'
84 self.hist_file = ':memory:'
84 self.log.error("Failed to load history too many times, history will not be saved.")
85 self.log.error("Failed to load history too many times, history will not be saved.")
85 elif self.hist_file.is_file():
86 elif self.hist_file.is_file():
86 # move the file out of the way
87 # move the file out of the way
87 base = str(self.hist_file.parent / self.hist_file.stem)
88 base = str(self.hist_file.parent / self.hist_file.stem)
88 ext = self.hist_file.suffix
89 ext = self.hist_file.suffix
89 size = self.hist_file.stat().st_size
90 size = self.hist_file.stat().st_size
90 if size >= _SAVE_DB_SIZE:
91 if size >= _SAVE_DB_SIZE:
91 # if there's significant content, avoid clobbering
92 # if there's significant content, avoid clobbering
92 now = datetime.datetime.now().isoformat().replace(':', '.')
93 now = datetime.datetime.now().isoformat().replace(':', '.')
93 newpath = base + '-corrupt-' + now + ext
94 newpath = base + '-corrupt-' + now + ext
94 # don't clobber previous corrupt backups
95 # don't clobber previous corrupt backups
95 for i in range(100):
96 for i in range(100):
96 if not Path(newpath).exists():
97 if not Path(newpath).exists():
97 break
98 break
98 else:
99 else:
99 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
100 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
100 else:
101 else:
101 # not much content, possibly empty; don't worry about clobbering
102 # not much content, possibly empty; don't worry about clobbering
102 # maybe we should just delete it?
103 # maybe we should just delete it?
103 newpath = base + '-corrupt' + ext
104 newpath = base + '-corrupt' + ext
104 self.hist_file.rename(newpath)
105 self.hist_file.rename(newpath)
105 self.log.error("History file was moved to %s and a new file created.", newpath)
106 self.log.error("History file was moved to %s and a new file created.", newpath)
106 self.init_db()
107 self.init_db()
107 return []
108 return []
108 else:
109 else:
109 # Failed with :memory:, something serious is wrong
110 # Failed with :memory:, something serious is wrong
110 raise
111 raise
111
112
112
113
113 class HistoryAccessorBase(LoggingConfigurable):
114 class HistoryAccessorBase(LoggingConfigurable):
114 """An abstract class for History Accessors """
115 """An abstract class for History Accessors """
115
116
116 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
117 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
117 raise NotImplementedError
118 raise NotImplementedError
118
119
119 def search(self, pattern="*", raw=True, search_raw=True,
120 def search(self, pattern="*", raw=True, search_raw=True,
120 output=False, n=None, unique=False):
121 output=False, n=None, unique=False):
121 raise NotImplementedError
122 raise NotImplementedError
122
123
123 def get_range(self, session, start=1, stop=None, raw=True,output=False):
124 def get_range(self, session, start=1, stop=None, raw=True,output=False):
124 raise NotImplementedError
125 raise NotImplementedError
125
126
126 def get_range_by_str(self, rangestr, raw=True, output=False):
127 def get_range_by_str(self, rangestr, raw=True, output=False):
127 raise NotImplementedError
128 raise NotImplementedError
128
129
129
130
130 class HistoryAccessor(HistoryAccessorBase):
131 class HistoryAccessor(HistoryAccessorBase):
131 """Access the history database without adding to it.
132 """Access the history database without adding to it.
132
133
133 This is intended for use by standalone history tools. IPython shells use
134 This is intended for use by standalone history tools. IPython shells use
134 HistoryManager, below, which is a subclass of this."""
135 HistoryManager, below, which is a subclass of this."""
135
136
136 # counter for init_db retries, so we don't keep trying over and over
137 # counter for init_db retries, so we don't keep trying over and over
137 _corrupt_db_counter = 0
138 _corrupt_db_counter = 0
138 # after two failures, fallback on :memory:
139 # after two failures, fallback on :memory:
139 _corrupt_db_limit = 2
140 _corrupt_db_limit = 2
140
141
141 # String holding the path to the history file
142 # String holding the path to the history file
142 hist_file = Union(
143 hist_file = Union(
143 [Instance(Path), Unicode()],
144 [Instance(Path), Unicode()],
144 help="""Path to file to use for SQLite history database.
145 help="""Path to file to use for SQLite history database.
145
146
146 By default, IPython will put the history database in the IPython
147 By default, IPython will put the history database in the IPython
147 profile directory. If you would rather share one history among
148 profile directory. If you would rather share one history among
148 profiles, you can set this value in each, so that they are consistent.
149 profiles, you can set this value in each, so that they are consistent.
149
150
150 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
151 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
151 mounts. If you see IPython hanging, try setting this to something on a
152 mounts. If you see IPython hanging, try setting this to something on a
152 local disk, e.g::
153 local disk, e.g::
153
154
154 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
155 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
155
156
156 you can also use the specific value `:memory:` (including the colon
157 you can also use the specific value `:memory:` (including the colon
157 at both end but not the back ticks), to avoid creating an history file.
158 at both end but not the back ticks), to avoid creating an history file.
158
159
159 """,
160 """,
160 ).tag(config=True)
161 ).tag(config=True)
161
162
162 enabled = Bool(True,
163 enabled = Bool(True,
163 help="""enable the SQLite history
164 help="""enable the SQLite history
164
165
165 set enabled=False to disable the SQLite history,
166 set enabled=False to disable the SQLite history,
166 in which case there will be no stored history, no SQLite connection,
167 in which case there will be no stored history, no SQLite connection,
167 and no background saving thread. This may be necessary in some
168 and no background saving thread. This may be necessary in some
168 threaded environments where IPython is embedded.
169 threaded environments where IPython is embedded.
169 """,
170 """,
170 ).tag(config=True)
171 ).tag(config=True)
171
172
172 connection_options = Dict(
173 connection_options = Dict(
173 help="""Options for configuring the SQLite connection
174 help="""Options for configuring the SQLite connection
174
175
175 These options are passed as keyword args to sqlite3.connect
176 These options are passed as keyword args to sqlite3.connect
176 when establishing database connections.
177 when establishing database connections.
177 """
178 """
178 ).tag(config=True)
179 ).tag(config=True)
179
180
180 @default("connection_options")
181 @default("connection_options")
181 def _default_connection_options(self):
182 def _default_connection_options(self):
182 return dict(check_same_thread=False)
183 return dict(check_same_thread=False)
183
184
184 # The SQLite database
185 # The SQLite database
185 db = Any()
186 db = Any()
186 @observe('db')
187 @observe('db')
187 def _db_changed(self, change):
188 def _db_changed(self, change):
188 """validate the db, since it can be an Instance of two different types"""
189 """validate the db, since it can be an Instance of two different types"""
189 new = change['new']
190 new = change['new']
190 connection_types = (DummyDB, sqlite3.Connection)
191 connection_types = (DummyDB, sqlite3.Connection)
191 if not isinstance(new, connection_types):
192 if not isinstance(new, connection_types):
192 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
193 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
193 (self.__class__.__name__, new)
194 (self.__class__.__name__, new)
194 raise TraitError(msg)
195 raise TraitError(msg)
195
196
196 def __init__(self, profile="default", hist_file="", **traits):
197 def __init__(self, profile="default", hist_file="", **traits):
197 """Create a new history accessor.
198 """Create a new history accessor.
198
199
199 Parameters
200 Parameters
200 ----------
201 ----------
201 profile : str
202 profile : str
202 The name of the profile from which to open history.
203 The name of the profile from which to open history.
203 hist_file : str
204 hist_file : str
204 Path to an SQLite history database stored by IPython. If specified,
205 Path to an SQLite history database stored by IPython. If specified,
205 hist_file overrides profile.
206 hist_file overrides profile.
206 config : :class:`~traitlets.config.loader.Config`
207 config : :class:`~traitlets.config.loader.Config`
207 Config object. hist_file can also be set through this.
208 Config object. hist_file can also be set through this.
208 """
209 """
209 super(HistoryAccessor, self).__init__(**traits)
210 super(HistoryAccessor, self).__init__(**traits)
210 # defer setting hist_file from kwarg until after init,
211 # defer setting hist_file from kwarg until after init,
211 # otherwise the default kwarg value would clobber any value
212 # otherwise the default kwarg value would clobber any value
212 # set by config
213 # set by config
213 if hist_file:
214 if hist_file:
214 self.hist_file = hist_file
215 self.hist_file = hist_file
215
216
216 try:
217 try:
217 self.hist_file
218 self.hist_file
218 except TraitError:
219 except TraitError:
219 # No one has set the hist_file, yet.
220 # No one has set the hist_file, yet.
220 self.hist_file = self._get_hist_file_name(profile)
221 self.hist_file = self._get_hist_file_name(profile)
221
222
222 self.init_db()
223 self.init_db()
223
224
224 def _get_hist_file_name(self, profile='default'):
225 def _get_hist_file_name(self, profile='default'):
225 """Find the history file for the given profile name.
226 """Find the history file for the given profile name.
226
227
227 This is overridden by the HistoryManager subclass, to use the shell's
228 This is overridden by the HistoryManager subclass, to use the shell's
228 active profile.
229 active profile.
229
230
230 Parameters
231 Parameters
231 ----------
232 ----------
232 profile : str
233 profile : str
233 The name of a profile which has a history file.
234 The name of a profile which has a history file.
234 """
235 """
235 return Path(locate_profile(profile)) / "history.sqlite"
236 return Path(locate_profile(profile)) / "history.sqlite"
236
237
237 @catch_corrupt_db
238 @catch_corrupt_db
238 def init_db(self):
239 def init_db(self):
239 """Connect to the database, and create tables if necessary."""
240 """Connect to the database, and create tables if necessary."""
240 if not self.enabled:
241 if not self.enabled:
241 self.db = DummyDB()
242 self.db = DummyDB()
242 return
243 return
243
244
244 # use detect_types so that timestamps return datetime objects
245 # use detect_types so that timestamps return datetime objects
245 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
246 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
246 kwargs.update(self.connection_options)
247 kwargs.update(self.connection_options)
247 self.db = sqlite3.connect(str(self.hist_file), **kwargs)
248 self.db = sqlite3.connect(str(self.hist_file), **kwargs)
248 with self.db:
249 with self.db:
249 self.db.execute(
250 self.db.execute(
250 """CREATE TABLE IF NOT EXISTS sessions (session integer
251 """CREATE TABLE IF NOT EXISTS sessions (session integer
251 primary key autoincrement, start timestamp,
252 primary key autoincrement, start timestamp,
252 end timestamp, num_cmds integer, remark text)"""
253 end timestamp, num_cmds integer, remark text)"""
253 )
254 )
254 self.db.execute(
255 self.db.execute(
255 """CREATE TABLE IF NOT EXISTS history
256 """CREATE TABLE IF NOT EXISTS history
256 (session integer, line integer, source text, source_raw text,
257 (session integer, line integer, source text, source_raw text,
257 PRIMARY KEY (session, line))"""
258 PRIMARY KEY (session, line))"""
258 )
259 )
259 # Output history is optional, but ensure the table's there so it can be
260 # Output history is optional, but ensure the table's there so it can be
260 # enabled later.
261 # enabled later.
261 self.db.execute(
262 self.db.execute(
262 """CREATE TABLE IF NOT EXISTS output_history
263 """CREATE TABLE IF NOT EXISTS output_history
263 (session integer, line integer, output text,
264 (session integer, line integer, output text,
264 PRIMARY KEY (session, line))"""
265 PRIMARY KEY (session, line))"""
265 )
266 )
266 # success! reset corrupt db count
267 # success! reset corrupt db count
267 self._corrupt_db_counter = 0
268 self._corrupt_db_counter = 0
268
269
269 def writeout_cache(self):
270 def writeout_cache(self):
270 """Overridden by HistoryManager to dump the cache before certain
271 """Overridden by HistoryManager to dump the cache before certain
271 database lookups."""
272 database lookups."""
272 pass
273 pass
273
274
274 ## -------------------------------
275 ## -------------------------------
275 ## Methods for retrieving history:
276 ## Methods for retrieving history:
276 ## -------------------------------
277 ## -------------------------------
277 def _run_sql(self, sql, params, raw=True, output=False, latest=False):
278 def _run_sql(self, sql, params, raw=True, output=False, latest=False):
278 """Prepares and runs an SQL query for the history database.
279 """Prepares and runs an SQL query for the history database.
279
280
280 Parameters
281 Parameters
281 ----------
282 ----------
282 sql : str
283 sql : str
283 Any filtering expressions to go after SELECT ... FROM ...
284 Any filtering expressions to go after SELECT ... FROM ...
284 params : tuple
285 params : tuple
285 Parameters passed to the SQL query (to replace "?")
286 Parameters passed to the SQL query (to replace "?")
286 raw, output : bool
287 raw, output : bool
287 See :meth:`get_range`
288 See :meth:`get_range`
288 latest : bool
289 latest : bool
289 Select rows with max (session, line)
290 Select rows with max (session, line)
290
291
291 Returns
292 Returns
292 -------
293 -------
293 Tuples as :meth:`get_range`
294 Tuples as :meth:`get_range`
294 """
295 """
295 toget = 'source_raw' if raw else 'source'
296 toget = 'source_raw' if raw else 'source'
296 sqlfrom = "history"
297 sqlfrom = "history"
297 if output:
298 if output:
298 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
299 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
299 toget = "history.%s, output_history.output" % toget
300 toget = "history.%s, output_history.output" % toget
300 if latest:
301 if latest:
301 toget += ", MAX(session * 128 * 1024 + line)"
302 toget += ", MAX(session * 128 * 1024 + line)"
302 this_querry = "SELECT session, line, %s FROM %s " % (toget, sqlfrom) + sql
303 this_querry = "SELECT session, line, %s FROM %s " % (toget, sqlfrom) + sql
303 cur = self.db.execute(this_querry, params)
304 cur = self.db.execute(this_querry, params)
304 if latest:
305 if latest:
305 cur = (row[:-1] for row in cur)
306 cur = (row[:-1] for row in cur)
306 if output: # Regroup into 3-tuples, and parse JSON
307 if output: # Regroup into 3-tuples, and parse JSON
307 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
308 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
308 return cur
309 return cur
309
310
310 @only_when_enabled
311 @only_when_enabled
311 @catch_corrupt_db
312 @catch_corrupt_db
312 def get_session_info(self, session):
313 def get_session_info(self, session):
313 """Get info about a session.
314 """Get info about a session.
314
315
315 Parameters
316 Parameters
316 ----------
317 ----------
317 session : int
318 session : int
318 Session number to retrieve.
319 Session number to retrieve.
319
320
320 Returns
321 Returns
321 -------
322 -------
322 session_id : int
323 session_id : int
323 Session ID number
324 Session ID number
324 start : datetime
325 start : datetime
325 Timestamp for the start of the session.
326 Timestamp for the start of the session.
326 end : datetime
327 end : datetime
327 Timestamp for the end of the session, or None if IPython crashed.
328 Timestamp for the end of the session, or None if IPython crashed.
328 num_cmds : int
329 num_cmds : int
329 Number of commands run, or None if IPython crashed.
330 Number of commands run, or None if IPython crashed.
330 remark : unicode
331 remark : unicode
331 A manually set description.
332 A manually set description.
332 """
333 """
333 query = "SELECT * from sessions where session == ?"
334 query = "SELECT * from sessions where session == ?"
334 return self.db.execute(query, (session,)).fetchone()
335 return self.db.execute(query, (session,)).fetchone()
335
336
336 @catch_corrupt_db
337 @catch_corrupt_db
337 def get_last_session_id(self):
338 def get_last_session_id(self):
338 """Get the last session ID currently in the database.
339 """Get the last session ID currently in the database.
339
340
340 Within IPython, this should be the same as the value stored in
341 Within IPython, this should be the same as the value stored in
341 :attr:`HistoryManager.session_number`.
342 :attr:`HistoryManager.session_number`.
342 """
343 """
343 for record in self.get_tail(n=1, include_latest=True):
344 for record in self.get_tail(n=1, include_latest=True):
344 return record[0]
345 return record[0]
345
346
346 @catch_corrupt_db
347 @catch_corrupt_db
347 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
348 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
348 """Get the last n lines from the history database.
349 """Get the last n lines from the history database.
349
350
350 Parameters
351 Parameters
351 ----------
352 ----------
352 n : int
353 n : int
353 The number of lines to get
354 The number of lines to get
354 raw, output : bool
355 raw, output : bool
355 See :meth:`get_range`
356 See :meth:`get_range`
356 include_latest : bool
357 include_latest : bool
357 If False (default), n+1 lines are fetched, and the latest one
358 If False (default), n+1 lines are fetched, and the latest one
358 is discarded. This is intended to be used where the function
359 is discarded. This is intended to be used where the function
359 is called by a user command, which it should not return.
360 is called by a user command, which it should not return.
360
361
361 Returns
362 Returns
362 -------
363 -------
363 Tuples as :meth:`get_range`
364 Tuples as :meth:`get_range`
364 """
365 """
365 self.writeout_cache()
366 self.writeout_cache()
366 if not include_latest:
367 if not include_latest:
367 n += 1
368 n += 1
368 cur = self._run_sql(
369 cur = self._run_sql(
369 "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output
370 "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output
370 )
371 )
371 if not include_latest:
372 if not include_latest:
372 return reversed(list(cur)[1:])
373 return reversed(list(cur)[1:])
373 return reversed(list(cur))
374 return reversed(list(cur))
374
375
375 @catch_corrupt_db
376 @catch_corrupt_db
376 def search(self, pattern="*", raw=True, search_raw=True,
377 def search(self, pattern="*", raw=True, search_raw=True,
377 output=False, n=None, unique=False):
378 output=False, n=None, unique=False):
378 """Search the database using unix glob-style matching (wildcards
379 """Search the database using unix glob-style matching (wildcards
379 * and ?).
380 * and ?).
380
381
381 Parameters
382 Parameters
382 ----------
383 ----------
383 pattern : str
384 pattern : str
384 The wildcarded pattern to match when searching
385 The wildcarded pattern to match when searching
385 search_raw : bool
386 search_raw : bool
386 If True, search the raw input, otherwise, the parsed input
387 If True, search the raw input, otherwise, the parsed input
387 raw, output : bool
388 raw, output : bool
388 See :meth:`get_range`
389 See :meth:`get_range`
389 n : None or int
390 n : None or int
390 If an integer is given, it defines the limit of
391 If an integer is given, it defines the limit of
391 returned entries.
392 returned entries.
392 unique : bool
393 unique : bool
393 When it is true, return only unique entries.
394 When it is true, return only unique entries.
394
395
395 Returns
396 Returns
396 -------
397 -------
397 Tuples as :meth:`get_range`
398 Tuples as :meth:`get_range`
398 """
399 """
399 tosearch = "source_raw" if search_raw else "source"
400 tosearch = "source_raw" if search_raw else "source"
400 if output:
401 if output:
401 tosearch = "history." + tosearch
402 tosearch = "history." + tosearch
402 self.writeout_cache()
403 self.writeout_cache()
403 sqlform = "WHERE %s GLOB ?" % tosearch
404 sqlform = "WHERE %s GLOB ?" % tosearch
404 params = (pattern,)
405 params = (pattern,)
405 if unique:
406 if unique:
406 sqlform += ' GROUP BY {0}'.format(tosearch)
407 sqlform += ' GROUP BY {0}'.format(tosearch)
407 if n is not None:
408 if n is not None:
408 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
409 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
409 params += (n,)
410 params += (n,)
410 elif unique:
411 elif unique:
411 sqlform += " ORDER BY session, line"
412 sqlform += " ORDER BY session, line"
412 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
413 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
413 if n is not None:
414 if n is not None:
414 return reversed(list(cur))
415 return reversed(list(cur))
415 return cur
416 return cur
416
417
417 @catch_corrupt_db
418 @catch_corrupt_db
418 def get_range(self, session, start=1, stop=None, raw=True,output=False):
419 def get_range(self, session, start=1, stop=None, raw=True,output=False):
419 """Retrieve input by session.
420 """Retrieve input by session.
420
421
421 Parameters
422 Parameters
422 ----------
423 ----------
423 session : int
424 session : int
424 Session number to retrieve.
425 Session number to retrieve.
425 start : int
426 start : int
426 First line to retrieve.
427 First line to retrieve.
427 stop : int
428 stop : int
428 End of line range (excluded from output itself). If None, retrieve
429 End of line range (excluded from output itself). If None, retrieve
429 to the end of the session.
430 to the end of the session.
430 raw : bool
431 raw : bool
431 If True, return untranslated input
432 If True, return untranslated input
432 output : bool
433 output : bool
433 If True, attempt to include output. This will be 'real' Python
434 If True, attempt to include output. This will be 'real' Python
434 objects for the current session, or text reprs from previous
435 objects for the current session, or text reprs from previous
435 sessions if db_log_output was enabled at the time. Where no output
436 sessions if db_log_output was enabled at the time. Where no output
436 is found, None is used.
437 is found, None is used.
437
438
438 Returns
439 Returns
439 -------
440 -------
440 entries
441 entries
441 An iterator over the desired lines. Each line is a 3-tuple, either
442 An iterator over the desired lines. Each line is a 3-tuple, either
442 (session, line, input) if output is False, or
443 (session, line, input) if output is False, or
443 (session, line, (input, output)) if output is True.
444 (session, line, (input, output)) if output is True.
444 """
445 """
445 if stop:
446 if stop:
446 lineclause = "line >= ? AND line < ?"
447 lineclause = "line >= ? AND line < ?"
447 params = (session, start, stop)
448 params = (session, start, stop)
448 else:
449 else:
449 lineclause = "line>=?"
450 lineclause = "line>=?"
450 params = (session, start)
451 params = (session, start)
451
452
452 return self._run_sql("WHERE session==? AND %s" % lineclause,
453 return self._run_sql("WHERE session==? AND %s" % lineclause,
453 params, raw=raw, output=output)
454 params, raw=raw, output=output)
454
455
455 def get_range_by_str(self, rangestr, raw=True, output=False):
456 def get_range_by_str(self, rangestr, raw=True, output=False):
456 """Get lines of history from a string of ranges, as used by magic
457 """Get lines of history from a string of ranges, as used by magic
457 commands %hist, %save, %macro, etc.
458 commands %hist, %save, %macro, etc.
458
459
459 Parameters
460 Parameters
460 ----------
461 ----------
461 rangestr : str
462 rangestr : str
462 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
463 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
463 this will return everything from current session's history.
464 this will return everything from current session's history.
464
465
465 See the documentation of :func:`%history` for the full details.
466 See the documentation of :func:`%history` for the full details.
466
467
467 raw, output : bool
468 raw, output : bool
468 As :meth:`get_range`
469 As :meth:`get_range`
469
470
470 Returns
471 Returns
471 -------
472 -------
472 Tuples as :meth:`get_range`
473 Tuples as :meth:`get_range`
473 """
474 """
474 for sess, s, e in extract_hist_ranges(rangestr):
475 for sess, s, e in extract_hist_ranges(rangestr):
475 for line in self.get_range(sess, s, e, raw=raw, output=output):
476 for line in self.get_range(sess, s, e, raw=raw, output=output):
476 yield line
477 yield line
477
478
478
479
479 class HistoryManager(HistoryAccessor):
480 class HistoryManager(HistoryAccessor):
480 """A class to organize all history-related functionality in one place.
481 """A class to organize all history-related functionality in one place.
481 """
482 """
482 # Public interface
483 # Public interface
483
484
484 # An instance of the IPython shell we are attached to
485 # An instance of the IPython shell we are attached to
485 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
486 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
486 allow_none=True)
487 allow_none=True)
487 # Lists to hold processed and raw history. These start with a blank entry
488 # Lists to hold processed and raw history. These start with a blank entry
488 # so that we can index them starting from 1
489 # so that we can index them starting from 1
489 input_hist_parsed = List([""])
490 input_hist_parsed = List([""])
490 input_hist_raw = List([""])
491 input_hist_raw = List([""])
491 # A list of directories visited during session
492 # A list of directories visited during session
492 dir_hist = List()
493 dir_hist = List()
493 @default('dir_hist')
494 @default('dir_hist')
494 def _dir_hist_default(self):
495 def _dir_hist_default(self):
495 try:
496 try:
496 return [Path.cwd()]
497 return [Path.cwd()]
497 except OSError:
498 except OSError:
498 return []
499 return []
499
500
500 # A dict of output history, keyed with ints from the shell's
501 # A dict of output history, keyed with ints from the shell's
501 # execution count.
502 # execution count.
502 output_hist = Dict()
503 output_hist = Dict()
503 # The text/plain repr of outputs.
504 # The text/plain repr of outputs.
504 output_hist_reprs = Dict()
505 output_hist_reprs = Dict()
505
506
506 # The number of the current session in the history database
507 # The number of the current session in the history database
507 session_number = Integer()
508 session_number = Integer()
508
509
509 db_log_output = Bool(False,
510 db_log_output = Bool(False,
510 help="Should the history database include output? (default: no)"
511 help="Should the history database include output? (default: no)"
511 ).tag(config=True)
512 ).tag(config=True)
512 db_cache_size = Integer(0,
513 db_cache_size = Integer(0,
513 help="Write to database every x commands (higher values save disk access & power).\n"
514 help="Write to database every x commands (higher values save disk access & power).\n"
514 "Values of 1 or less effectively disable caching."
515 "Values of 1 or less effectively disable caching."
515 ).tag(config=True)
516 ).tag(config=True)
516 # The input and output caches
517 # The input and output caches
517 db_input_cache = List()
518 db_input_cache = List()
518 db_output_cache = List()
519 db_output_cache = List()
519
520
520 # History saving in separate thread
521 # History saving in separate thread
521 save_thread = Instance('IPython.core.history.HistorySavingThread',
522 save_thread = Instance('IPython.core.history.HistorySavingThread',
522 allow_none=True)
523 allow_none=True)
523 save_flag = Instance(threading.Event, allow_none=True)
524 save_flag = Instance(threading.Event, allow_none=True)
524
525
525 # Private interface
526 # Private interface
526 # Variables used to store the three last inputs from the user. On each new
527 # Variables used to store the three last inputs from the user. On each new
527 # history update, we populate the user's namespace with these, shifted as
528 # history update, we populate the user's namespace with these, shifted as
528 # necessary.
529 # necessary.
529 _i00 = Unicode(u'')
530 _i00 = Unicode(u'')
530 _i = Unicode(u'')
531 _i = Unicode(u'')
531 _ii = Unicode(u'')
532 _ii = Unicode(u'')
532 _iii = Unicode(u'')
533 _iii = Unicode(u'')
533
534
534 # A regex matching all forms of the exit command, so that we don't store
535 # A regex matching all forms of the exit command, so that we don't store
535 # them in the history (it's annoying to rewind the first entry and land on
536 # them in the history (it's annoying to rewind the first entry and land on
536 # an exit call).
537 # an exit call).
537 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
538 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
538
539
539 def __init__(self, shell=None, config=None, **traits):
540 def __init__(self, shell=None, config=None, **traits):
540 """Create a new history manager associated with a shell instance.
541 """Create a new history manager associated with a shell instance.
541 """
542 """
542 super(HistoryManager, self).__init__(shell=shell, config=config,
543 super(HistoryManager, self).__init__(shell=shell, config=config,
543 **traits)
544 **traits)
544 self.save_flag = threading.Event()
545 self.save_flag = threading.Event()
545 self.db_input_cache_lock = threading.Lock()
546 self.db_input_cache_lock = threading.Lock()
546 self.db_output_cache_lock = threading.Lock()
547 self.db_output_cache_lock = threading.Lock()
547
548
548 try:
549 try:
549 self.new_session()
550 self.new_session()
550 except sqlite3.OperationalError:
551 except sqlite3.OperationalError:
551 self.log.error("Failed to create history session in %s. History will not be saved.",
552 self.log.error("Failed to create history session in %s. History will not be saved.",
552 self.hist_file, exc_info=True)
553 self.hist_file, exc_info=True)
553 self.hist_file = ':memory:'
554 self.hist_file = ':memory:'
554
555
555 if self.enabled and self.hist_file != ':memory:':
556 if self.enabled and self.hist_file != ':memory:':
556 self.save_thread = HistorySavingThread(self)
557 self.save_thread = HistorySavingThread(self)
557 self.save_thread.start()
558 try:
559 self.save_thread.start()
560 except RuntimeError:
561 self.log.error(
562 "Failed to start history saving thread. History will not be saved.",
563 exc_info=True,
564 )
565 self.hist_file = ":memory:"
558
566
559 def _get_hist_file_name(self, profile=None):
567 def _get_hist_file_name(self, profile=None):
560 """Get default history file name based on the Shell's profile.
568 """Get default history file name based on the Shell's profile.
561
569
562 The profile parameter is ignored, but must exist for compatibility with
570 The profile parameter is ignored, but must exist for compatibility with
563 the parent class."""
571 the parent class."""
564 profile_dir = self.shell.profile_dir.location
572 profile_dir = self.shell.profile_dir.location
565 return Path(profile_dir) / "history.sqlite"
573 return Path(profile_dir) / "history.sqlite"
566
574
567 @only_when_enabled
575 @only_when_enabled
568 def new_session(self, conn=None):
576 def new_session(self, conn=None):
569 """Get a new session number."""
577 """Get a new session number."""
570 if conn is None:
578 if conn is None:
571 conn = self.db
579 conn = self.db
572
580
573 with conn:
581 with conn:
574 cur = conn.execute(
582 cur = conn.execute(
575 """INSERT INTO sessions VALUES (NULL, ?, NULL,
583 """INSERT INTO sessions VALUES (NULL, ?, NULL,
576 NULL, '') """,
584 NULL, '') """,
577 (datetime.datetime.now().isoformat(" "),),
585 (datetime.datetime.now().isoformat(" "),),
578 )
586 )
579 self.session_number = cur.lastrowid
587 self.session_number = cur.lastrowid
580
588
581 def end_session(self):
589 def end_session(self):
582 """Close the database session, filling in the end time and line count."""
590 """Close the database session, filling in the end time and line count."""
583 self.writeout_cache()
591 self.writeout_cache()
584 with self.db:
592 with self.db:
585 self.db.execute(
593 self.db.execute(
586 """UPDATE sessions SET end=?, num_cmds=? WHERE
594 """UPDATE sessions SET end=?, num_cmds=? WHERE
587 session==?""",
595 session==?""",
588 (
596 (
589 datetime.datetime.now().isoformat(" "),
597 datetime.datetime.now().isoformat(" "),
590 len(self.input_hist_parsed) - 1,
598 len(self.input_hist_parsed) - 1,
591 self.session_number,
599 self.session_number,
592 ),
600 ),
593 )
601 )
594 self.session_number = 0
602 self.session_number = 0
595
603
596 def name_session(self, name):
604 def name_session(self, name):
597 """Give the current session a name in the history database."""
605 """Give the current session a name in the history database."""
598 with self.db:
606 with self.db:
599 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
607 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
600 (name, self.session_number))
608 (name, self.session_number))
601
609
602 def reset(self, new_session=True):
610 def reset(self, new_session=True):
603 """Clear the session history, releasing all object references, and
611 """Clear the session history, releasing all object references, and
604 optionally open a new session."""
612 optionally open a new session."""
605 self.output_hist.clear()
613 self.output_hist.clear()
606 # The directory history can't be completely empty
614 # The directory history can't be completely empty
607 self.dir_hist[:] = [Path.cwd()]
615 self.dir_hist[:] = [Path.cwd()]
608
616
609 if new_session:
617 if new_session:
610 if self.session_number:
618 if self.session_number:
611 self.end_session()
619 self.end_session()
612 self.input_hist_parsed[:] = [""]
620 self.input_hist_parsed[:] = [""]
613 self.input_hist_raw[:] = [""]
621 self.input_hist_raw[:] = [""]
614 self.new_session()
622 self.new_session()
615
623
616 # ------------------------------
624 # ------------------------------
617 # Methods for retrieving history
625 # Methods for retrieving history
618 # ------------------------------
626 # ------------------------------
619 def get_session_info(self, session=0):
627 def get_session_info(self, session=0):
620 """Get info about a session.
628 """Get info about a session.
621
629
622 Parameters
630 Parameters
623 ----------
631 ----------
624 session : int
632 session : int
625 Session number to retrieve. The current session is 0, and negative
633 Session number to retrieve. The current session is 0, and negative
626 numbers count back from current session, so -1 is the previous session.
634 numbers count back from current session, so -1 is the previous session.
627
635
628 Returns
636 Returns
629 -------
637 -------
630 session_id : int
638 session_id : int
631 Session ID number
639 Session ID number
632 start : datetime
640 start : datetime
633 Timestamp for the start of the session.
641 Timestamp for the start of the session.
634 end : datetime
642 end : datetime
635 Timestamp for the end of the session, or None if IPython crashed.
643 Timestamp for the end of the session, or None if IPython crashed.
636 num_cmds : int
644 num_cmds : int
637 Number of commands run, or None if IPython crashed.
645 Number of commands run, or None if IPython crashed.
638 remark : unicode
646 remark : unicode
639 A manually set description.
647 A manually set description.
640 """
648 """
641 if session <= 0:
649 if session <= 0:
642 session += self.session_number
650 session += self.session_number
643
651
644 return super(HistoryManager, self).get_session_info(session=session)
652 return super(HistoryManager, self).get_session_info(session=session)
645
653
646 @catch_corrupt_db
654 @catch_corrupt_db
647 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
655 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
648 """Get the last n lines from the history database.
656 """Get the last n lines from the history database.
649
657
650 Most recent entry last.
658 Most recent entry last.
651
659
652 Completion will be reordered so that that the last ones are when
660 Completion will be reordered so that that the last ones are when
653 possible from current session.
661 possible from current session.
654
662
655 Parameters
663 Parameters
656 ----------
664 ----------
657 n : int
665 n : int
658 The number of lines to get
666 The number of lines to get
659 raw, output : bool
667 raw, output : bool
660 See :meth:`get_range`
668 See :meth:`get_range`
661 include_latest : bool
669 include_latest : bool
662 If False (default), n+1 lines are fetched, and the latest one
670 If False (default), n+1 lines are fetched, and the latest one
663 is discarded. This is intended to be used where the function
671 is discarded. This is intended to be used where the function
664 is called by a user command, which it should not return.
672 is called by a user command, which it should not return.
665
673
666 Returns
674 Returns
667 -------
675 -------
668 Tuples as :meth:`get_range`
676 Tuples as :meth:`get_range`
669 """
677 """
670 self.writeout_cache()
678 self.writeout_cache()
671 if not include_latest:
679 if not include_latest:
672 n += 1
680 n += 1
673 # cursor/line/entry
681 # cursor/line/entry
674 this_cur = list(
682 this_cur = list(
675 self._run_sql(
683 self._run_sql(
676 "WHERE session == ? ORDER BY line DESC LIMIT ? ",
684 "WHERE session == ? ORDER BY line DESC LIMIT ? ",
677 (self.session_number, n),
685 (self.session_number, n),
678 raw=raw,
686 raw=raw,
679 output=output,
687 output=output,
680 )
688 )
681 )
689 )
682 other_cur = list(
690 other_cur = list(
683 self._run_sql(
691 self._run_sql(
684 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
692 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
685 (self.session_number, n),
693 (self.session_number, n),
686 raw=raw,
694 raw=raw,
687 output=output,
695 output=output,
688 )
696 )
689 )
697 )
690
698
691 everything = this_cur + other_cur
699 everything = this_cur + other_cur
692
700
693 everything = everything[:n]
701 everything = everything[:n]
694
702
695 if not include_latest:
703 if not include_latest:
696 return list(everything)[:0:-1]
704 return list(everything)[:0:-1]
697 return list(everything)[::-1]
705 return list(everything)[::-1]
698
706
699 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
707 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
700 """Get input and output history from the current session. Called by
708 """Get input and output history from the current session. Called by
701 get_range, and takes similar parameters."""
709 get_range, and takes similar parameters."""
702 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
710 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
703
711
704 n = len(input_hist)
712 n = len(input_hist)
705 if start < 0:
713 if start < 0:
706 start += n
714 start += n
707 if not stop or (stop > n):
715 if not stop or (stop > n):
708 stop = n
716 stop = n
709 elif stop < 0:
717 elif stop < 0:
710 stop += n
718 stop += n
711
719
712 for i in range(start, stop):
720 for i in range(start, stop):
713 if output:
721 if output:
714 line = (input_hist[i], self.output_hist_reprs.get(i))
722 line = (input_hist[i], self.output_hist_reprs.get(i))
715 else:
723 else:
716 line = input_hist[i]
724 line = input_hist[i]
717 yield (0, i, line)
725 yield (0, i, line)
718
726
719 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
727 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
720 """Retrieve input by session.
728 """Retrieve input by session.
721
729
722 Parameters
730 Parameters
723 ----------
731 ----------
724 session : int
732 session : int
725 Session number to retrieve. The current session is 0, and negative
733 Session number to retrieve. The current session is 0, and negative
726 numbers count back from current session, so -1 is previous session.
734 numbers count back from current session, so -1 is previous session.
727 start : int
735 start : int
728 First line to retrieve.
736 First line to retrieve.
729 stop : int
737 stop : int
730 End of line range (excluded from output itself). If None, retrieve
738 End of line range (excluded from output itself). If None, retrieve
731 to the end of the session.
739 to the end of the session.
732 raw : bool
740 raw : bool
733 If True, return untranslated input
741 If True, return untranslated input
734 output : bool
742 output : bool
735 If True, attempt to include output. This will be 'real' Python
743 If True, attempt to include output. This will be 'real' Python
736 objects for the current session, or text reprs from previous
744 objects for the current session, or text reprs from previous
737 sessions if db_log_output was enabled at the time. Where no output
745 sessions if db_log_output was enabled at the time. Where no output
738 is found, None is used.
746 is found, None is used.
739
747
740 Returns
748 Returns
741 -------
749 -------
742 entries
750 entries
743 An iterator over the desired lines. Each line is a 3-tuple, either
751 An iterator over the desired lines. Each line is a 3-tuple, either
744 (session, line, input) if output is False, or
752 (session, line, input) if output is False, or
745 (session, line, (input, output)) if output is True.
753 (session, line, (input, output)) if output is True.
746 """
754 """
747 if session <= 0:
755 if session <= 0:
748 session += self.session_number
756 session += self.session_number
749 if session==self.session_number: # Current session
757 if session==self.session_number: # Current session
750 return self._get_range_session(start, stop, raw, output)
758 return self._get_range_session(start, stop, raw, output)
751 return super(HistoryManager, self).get_range(session, start, stop, raw,
759 return super(HistoryManager, self).get_range(session, start, stop, raw,
752 output)
760 output)
753
761
754 ## ----------------------------
762 ## ----------------------------
755 ## Methods for storing history:
763 ## Methods for storing history:
756 ## ----------------------------
764 ## ----------------------------
757 def store_inputs(self, line_num, source, source_raw=None):
765 def store_inputs(self, line_num, source, source_raw=None):
758 """Store source and raw input in history and create input cache
766 """Store source and raw input in history and create input cache
759 variables ``_i*``.
767 variables ``_i*``.
760
768
761 Parameters
769 Parameters
762 ----------
770 ----------
763 line_num : int
771 line_num : int
764 The prompt number of this input.
772 The prompt number of this input.
765 source : str
773 source : str
766 Python input.
774 Python input.
767 source_raw : str, optional
775 source_raw : str, optional
768 If given, this is the raw input without any IPython transformations
776 If given, this is the raw input without any IPython transformations
769 applied to it. If not given, ``source`` is used.
777 applied to it. If not given, ``source`` is used.
770 """
778 """
771 if source_raw is None:
779 if source_raw is None:
772 source_raw = source
780 source_raw = source
773 source = source.rstrip('\n')
781 source = source.rstrip('\n')
774 source_raw = source_raw.rstrip('\n')
782 source_raw = source_raw.rstrip('\n')
775
783
776 # do not store exit/quit commands
784 # do not store exit/quit commands
777 if self._exit_re.match(source_raw.strip()):
785 if self._exit_re.match(source_raw.strip()):
778 return
786 return
779
787
780 self.input_hist_parsed.append(source)
788 self.input_hist_parsed.append(source)
781 self.input_hist_raw.append(source_raw)
789 self.input_hist_raw.append(source_raw)
782
790
783 with self.db_input_cache_lock:
791 with self.db_input_cache_lock:
784 self.db_input_cache.append((line_num, source, source_raw))
792 self.db_input_cache.append((line_num, source, source_raw))
785 # Trigger to flush cache and write to DB.
793 # Trigger to flush cache and write to DB.
786 if len(self.db_input_cache) >= self.db_cache_size:
794 if len(self.db_input_cache) >= self.db_cache_size:
787 self.save_flag.set()
795 self.save_flag.set()
788
796
789 # update the auto _i variables
797 # update the auto _i variables
790 self._iii = self._ii
798 self._iii = self._ii
791 self._ii = self._i
799 self._ii = self._i
792 self._i = self._i00
800 self._i = self._i00
793 self._i00 = source_raw
801 self._i00 = source_raw
794
802
795 # hackish access to user namespace to create _i1,_i2... dynamically
803 # hackish access to user namespace to create _i1,_i2... dynamically
796 new_i = '_i%s' % line_num
804 new_i = '_i%s' % line_num
797 to_main = {'_i': self._i,
805 to_main = {'_i': self._i,
798 '_ii': self._ii,
806 '_ii': self._ii,
799 '_iii': self._iii,
807 '_iii': self._iii,
800 new_i : self._i00 }
808 new_i : self._i00 }
801
809
802 if self.shell is not None:
810 if self.shell is not None:
803 self.shell.push(to_main, interactive=False)
811 self.shell.push(to_main, interactive=False)
804
812
805 def store_output(self, line_num):
813 def store_output(self, line_num):
806 """If database output logging is enabled, this saves all the
814 """If database output logging is enabled, this saves all the
807 outputs from the indicated prompt number to the database. It's
815 outputs from the indicated prompt number to the database. It's
808 called by run_cell after code has been executed.
816 called by run_cell after code has been executed.
809
817
810 Parameters
818 Parameters
811 ----------
819 ----------
812 line_num : int
820 line_num : int
813 The line number from which to save outputs
821 The line number from which to save outputs
814 """
822 """
815 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
823 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
816 return
824 return
817 output = self.output_hist_reprs[line_num]
825 output = self.output_hist_reprs[line_num]
818
826
819 with self.db_output_cache_lock:
827 with self.db_output_cache_lock:
820 self.db_output_cache.append((line_num, output))
828 self.db_output_cache.append((line_num, output))
821 if self.db_cache_size <= 1:
829 if self.db_cache_size <= 1:
822 self.save_flag.set()
830 self.save_flag.set()
823
831
824 def _writeout_input_cache(self, conn):
832 def _writeout_input_cache(self, conn):
825 with conn:
833 with conn:
826 for line in self.db_input_cache:
834 for line in self.db_input_cache:
827 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
835 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
828 (self.session_number,)+line)
836 (self.session_number,)+line)
829
837
830 def _writeout_output_cache(self, conn):
838 def _writeout_output_cache(self, conn):
831 with conn:
839 with conn:
832 for line in self.db_output_cache:
840 for line in self.db_output_cache:
833 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
841 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
834 (self.session_number,)+line)
842 (self.session_number,)+line)
835
843
836 @only_when_enabled
844 @only_when_enabled
837 def writeout_cache(self, conn=None):
845 def writeout_cache(self, conn=None):
838 """Write any entries in the cache to the database."""
846 """Write any entries in the cache to the database."""
839 if conn is None:
847 if conn is None:
840 conn = self.db
848 conn = self.db
841
849
842 with self.db_input_cache_lock:
850 with self.db_input_cache_lock:
843 try:
851 try:
844 self._writeout_input_cache(conn)
852 self._writeout_input_cache(conn)
845 except sqlite3.IntegrityError:
853 except sqlite3.IntegrityError:
846 self.new_session(conn)
854 self.new_session(conn)
847 print("ERROR! Session/line number was not unique in",
855 print("ERROR! Session/line number was not unique in",
848 "database. History logging moved to new session",
856 "database. History logging moved to new session",
849 self.session_number)
857 self.session_number)
850 try:
858 try:
851 # Try writing to the new session. If this fails, don't
859 # Try writing to the new session. If this fails, don't
852 # recurse
860 # recurse
853 self._writeout_input_cache(conn)
861 self._writeout_input_cache(conn)
854 except sqlite3.IntegrityError:
862 except sqlite3.IntegrityError:
855 pass
863 pass
856 finally:
864 finally:
857 self.db_input_cache = []
865 self.db_input_cache = []
858
866
859 with self.db_output_cache_lock:
867 with self.db_output_cache_lock:
860 try:
868 try:
861 self._writeout_output_cache(conn)
869 self._writeout_output_cache(conn)
862 except sqlite3.IntegrityError:
870 except sqlite3.IntegrityError:
863 print("!! Session/line number for output was not unique",
871 print("!! Session/line number for output was not unique",
864 "in database. Output will not be stored.")
872 "in database. Output will not be stored.")
865 finally:
873 finally:
866 self.db_output_cache = []
874 self.db_output_cache = []
867
875
868
876
869 class HistorySavingThread(threading.Thread):
877 class HistorySavingThread(threading.Thread):
870 """This thread takes care of writing history to the database, so that
878 """This thread takes care of writing history to the database, so that
871 the UI isn't held up while that happens.
879 the UI isn't held up while that happens.
872
880
873 It waits for the HistoryManager's save_flag to be set, then writes out
881 It waits for the HistoryManager's save_flag to be set, then writes out
874 the history cache. The main thread is responsible for setting the flag when
882 the history cache. The main thread is responsible for setting the flag when
875 the cache size reaches a defined threshold."""
883 the cache size reaches a defined threshold."""
876 daemon = True
884 daemon = True
877 stop_now = False
885 stop_now = False
878 enabled = True
886 enabled = True
879 def __init__(self, history_manager):
887 def __init__(self, history_manager):
880 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
888 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
881 self.history_manager = history_manager
889 self.history_manager = history_manager
882 self.enabled = history_manager.enabled
890 self.enabled = history_manager.enabled
883 atexit.register(self.stop)
884
891
885 @only_when_enabled
892 @only_when_enabled
886 def run(self):
893 def run(self):
894 atexit.register(self.stop)
887 # We need a separate db connection per thread:
895 # We need a separate db connection per thread:
888 try:
896 try:
889 self.db = sqlite3.connect(
897 self.db = sqlite3.connect(
890 str(self.history_manager.hist_file),
898 str(self.history_manager.hist_file),
891 **self.history_manager.connection_options,
899 **self.history_manager.connection_options,
892 )
900 )
893 while True:
901 while True:
894 self.history_manager.save_flag.wait()
902 self.history_manager.save_flag.wait()
895 if self.stop_now:
903 if self.stop_now:
896 self.db.close()
904 self.db.close()
897 return
905 return
898 self.history_manager.save_flag.clear()
906 self.history_manager.save_flag.clear()
899 self.history_manager.writeout_cache(self.db)
907 self.history_manager.writeout_cache(self.db)
900 except Exception as e:
908 except Exception as e:
901 print(("The history saving thread hit an unexpected error (%s)."
909 print(("The history saving thread hit an unexpected error (%s)."
902 "History will not be written to the database.") % repr(e))
910 "History will not be written to the database.") % repr(e))
911 finally:
912 atexit.unregister(self.stop)
903
913
904 def stop(self):
914 def stop(self):
905 """This can be called from the main thread to safely stop this thread.
915 """This can be called from the main thread to safely stop this thread.
906
916
907 Note that it does not attempt to write out remaining history before
917 Note that it does not attempt to write out remaining history before
908 exiting. That should be done by calling the HistoryManager's
918 exiting. That should be done by calling the HistoryManager's
909 end_session method."""
919 end_session method."""
910 self.stop_now = True
920 self.stop_now = True
911 self.history_manager.save_flag.set()
921 self.history_manager.save_flag.set()
912 self.join()
922 self.join()
913
923
914
924
915 # To match, e.g. ~5/8-~2/3
925 # To match, e.g. ~5/8-~2/3
916 range_re = re.compile(r"""
926 range_re = re.compile(r"""
917 ((?P<startsess>~?\d+)/)?
927 ((?P<startsess>~?\d+)/)?
918 (?P<start>\d+)?
928 (?P<start>\d+)?
919 ((?P<sep>[\-:])
929 ((?P<sep>[\-:])
920 ((?P<endsess>~?\d+)/)?
930 ((?P<endsess>~?\d+)/)?
921 (?P<end>\d+))?
931 (?P<end>\d+))?
922 $""", re.VERBOSE)
932 $""", re.VERBOSE)
923
933
924
934
925 def extract_hist_ranges(ranges_str):
935 def extract_hist_ranges(ranges_str):
926 """Turn a string of history ranges into 3-tuples of (session, start, stop).
936 """Turn a string of history ranges into 3-tuples of (session, start, stop).
927
937
928 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
938 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
929 session".
939 session".
930
940
931 Examples
941 Examples
932 --------
942 --------
933 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
943 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
934 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
944 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
935 """
945 """
936 if ranges_str == "":
946 if ranges_str == "":
937 yield (0, 1, None) # Everything from current session
947 yield (0, 1, None) # Everything from current session
938 return
948 return
939
949
940 for range_str in ranges_str.split():
950 for range_str in ranges_str.split():
941 rmatch = range_re.match(range_str)
951 rmatch = range_re.match(range_str)
942 if not rmatch:
952 if not rmatch:
943 continue
953 continue
944 start = rmatch.group("start")
954 start = rmatch.group("start")
945 if start:
955 if start:
946 start = int(start)
956 start = int(start)
947 end = rmatch.group("end")
957 end = rmatch.group("end")
948 # If no end specified, get (a, a + 1)
958 # If no end specified, get (a, a + 1)
949 end = int(end) if end else start + 1
959 end = int(end) if end else start + 1
950 else: # start not specified
960 else: # start not specified
951 if not rmatch.group('startsess'): # no startsess
961 if not rmatch.group('startsess'): # no startsess
952 continue
962 continue
953 start = 1
963 start = 1
954 end = None # provide the entire session hist
964 end = None # provide the entire session hist
955
965
956 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
966 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
957 end += 1
967 end += 1
958 startsess = rmatch.group("startsess") or "0"
968 startsess = rmatch.group("startsess") or "0"
959 endsess = rmatch.group("endsess") or startsess
969 endsess = rmatch.group("endsess") or startsess
960 startsess = int(startsess.replace("~","-"))
970 startsess = int(startsess.replace("~","-"))
961 endsess = int(endsess.replace("~","-"))
971 endsess = int(endsess.replace("~","-"))
962 assert endsess >= startsess, "start session must be earlier than end session"
972 assert endsess >= startsess, "start session must be earlier than end session"
963
973
964 if endsess == startsess:
974 if endsess == startsess:
965 yield (startsess, start, end)
975 yield (startsess, start, end)
966 continue
976 continue
967 # Multiple sessions in one range:
977 # Multiple sessions in one range:
968 yield (startsess, start, None)
978 yield (startsess, start, None)
969 for sess in range(startsess+1, endsess):
979 for sess in range(startsess+1, endsess):
970 yield (sess, 1, None)
980 yield (sess, 1, None)
971 yield (endsess, 1, end)
981 yield (endsess, 1, end)
972
982
973
983
974 def _format_lineno(session, line):
984 def _format_lineno(session, line):
975 """Helper function to format line numbers properly."""
985 """Helper function to format line numbers properly."""
976 if session == 0:
986 if session == 0:
977 return str(line)
987 return str(line)
978 return "%s#%s" % (session, line)
988 return "%s#%s" % (session, line)
General Comments 0
You need to be logged in to leave comments. Login now