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