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