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