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